使用SpringBoot进行身份验证和授权的示例详解

 更新时间:2023年11月30日 16:56:42   作者:happyEnding  
在广阔的 Web 开发世界中,身份验证是每个数字领域的守护者,在本教程中,我们将了解如何以本机方式保护、验证和授权 Spring-Boot 应用程序的用户,并遵循框架的良好实践,希望对大家有所帮助

第一步

为了增强我们的应用程序的安全性,我们需要两个依赖项pom.xml,第一个是spring-security,另一个将帮助我们创建和验证 jwt 令牌。

//pom.xml
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
   <groupId>com.auth0</groupId>
   <artifactId>java-jwt</artifactId>
   <version>4.4.0</version>
</dependency>

用户实体和存储库

首先,我们需要一个枚举来表示用户角色,这将帮助我们定义应用程序中每个用户的权限。

// enums/UserRole.java
public enum UserRole {
  ADMIN("admin"),
  USER("user");

  private String role;

  UserRole(String role) {
    this.role = role;
  }

  public String getValue() {
    return role;
  }
}

在枚举中,我们有两个代表性角色:ADMINUSER,该ADMIN角色将有权访问我们应用程序的所有接口,而该USER角色只能访问特定接口。

用户实体将是我们身份验证系统的核心,它将保存用户的凭据和用户拥有的角色。我们将实现UserDetails接口来表示我们的用户实体,该接口由 spring security 包提供,并且是在 spring-boot 应用程序中表示用户实体的推荐方法。

// entities/UserEntity.java
@Table()
@Entity(name = "users")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class User implements UserDetails {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String login;

  private String password;

  @Enumerated(EnumType.STRING)
  private UserRole role;

  public User(String login, String password, UserRole role) {
    this.login = login;
    this.password = password;
    this.role = role;
  }

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    if (this.role == UserRole.ADMIN) {
      return List.of(new SimpleGrantedAuthority("ROLE_ADMIN"), new SimpleGrantedAuthority("ROLE_USER"));
    }
    return List.of(new SimpleGrantedAuthority("ROLE_USER"));
  }

  @Override
  public String getUsername() {
    return login;
  }

  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  @Override
  public boolean isEnabled() {
    return true;
  }
}

它有很多我们可以覆盖的方法来自定义身份验证过程,您也可以在数据库中实现这些属性,但现在我们只使用使我们的身份验证系统工作所需的方法:idusername和。password``role

对于用户存储库,我们有以下代码:

// repositories/UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
  UserDetails findByLogin(String login);
}

扩展后,JpaRepository我们将可以访问许多方法来操作数据库中的用户。此外,findByLoginSpring Security 将使用该方法在数据库中查找用户并验证凭据。

密钥

我们需要定义一个密钥来签署我们的令牌,该密钥将用于验证和生成令牌签名。我们将使用@Value注释从文件中获取密钥application.yml。在文件中,application.yml我们将密钥定义为环境变量,这将帮助我们保证密钥的安全并远离源代码。

//.env
JWT_SECRET="yoursecret"

在我们的application.yml

// resources/application.yml
security:
  jwt:
    token:
      secret-key: ${JWT_SECRET}

为了让 spring-boot 应用程序读取环境变量,我们需要PropertySource在主类中声明注释来指示.env文件所在的位置。在我们的例子中,它位于项目的根目录中,因此我们将使用该user.dir变量来获取项目根路径。主类将如下所示:

@SpringBootApplication
@PropertySource("file:${user.dir}/.env")
public class SpringAuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringAuthApplication.class, args);
    }
}

最后我们可以定义我们的令牌提供者类,该类将负责生成和验证我们的令牌。

// config/auth/TokenProvider.java
@Service
public class TokenProvider {
  @Value("${security.jwt.token.secret-key}")
  private String JWT_SECRET;

  public String generateAccessToken(User user) {
    try {
      Algorithm algorithm = Algorithm.HMAC256(JWT_SECRET);
      return JWT.create()
          .withSubject(user.getUsername())
          .withClaim("username", user.getUsername())
          .withExpiresAt(genAccessExpirationDate())
          .sign(algorithm);
    } catch (JWTCreationException exception) {
      throw new JWTCreationException("Error while generating token", exception);
    }
  }

  public String validateToken(String token) {
    try {
      Algorithm algorithm = Algorithm.HMAC256(JWT_SECRET);
      return JWT.require(algorithm)
          .build()
          .verify(token)
          .getSubject();
    } catch (JWTVerificationException exception) {
      throw new JWTVerificationException("Error while validating token", exception);
    }
  }

  private Instant genAccessExpirationDate() {
    return LocalDateTime.now().plusHours(2).toInstant(ZoneOffset.of("-03:00"));
  }
}

在本文中,generateAccessToken我们定义了一个算法来签署我们的令牌、令牌的主题和到期日期,并返回一个新的令牌。在该validateToken方法中,我们验证令牌签名并返回令牌的主题。

安全过滤器

然后我们需要定义一个过滤器来拦截请求并验证令牌。我们将扩展OncePerRequestFilterspring 安全类来拦截请求并验证令牌。

// config/auth/SecurityFilter.java
@Component
public class SecurityFilter extends OncePerRequestFilter {
  @Autowired
  TokenProvider tokenService;
  @Autowired
  UserRepository userRepository;

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
    var token = this.recoverToken(request);
    if (token != null) {
      var login = tokenService.validateToken(token);
      var user = userRepository.findByLogin(login);
      var authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
      SecurityContextHolder.getContext().setAuthentication(authentication);
    }
    filterChain.doFilter(request, response);
  }

  private String recoverToken(HttpServletRequest request) {
    var authHeader = request.getHeader("Authorization");
    if (authHeader == null)
      return null;
    return authHeader.replace("Bearer ", "");
  }
}

在该doFilterInternal方法中,我们从请求中恢复令牌,使用辅助方法从字符串中删除“Bearer” recoverToken,验证令牌并在SecurityContextHolder. 这SecurityContextHolder是一个 Spring Security 类,它保存当前请求的身份验证,因此我们可以访问控制器中的用户信息。

认证配置

这里我们需要定义一些更必要的方法来使我们的身份验证系统正常工作。在顶部,我们有Configuration@EnableWebSecurity注释,用于在我们的应用程序中启用网络安全。然后我们定义SecurityFilterChainbean 来定义将受我们的身份验证系统保护的接口。

// config/AuthConfig.java
@Configuration
@EnableWebSecurity
public class AuthConfig {
  @Autowired
  SecurityFilter securityFilter;

  @Bean
  SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
    return httpSecurity
        .csrf(csrf -> csrf.disable())
        .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(authorize -> authorize
            .requestMatchers(HttpMethod.POST, "/api/v1/auth/*").permitAll()
            .requestMatchers(HttpMethod.POST, "/api/v1/books").hasRole("ADMIN")
            .anyRequest().authenticated())
        .addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)
        .build();
  }

  @Bean
  AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
      throws Exception {
    return authenticationConfiguration.getAuthenticationManager();
  }

  @Bean
  PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }
}

在该authorizeHttpRequests方法中,我们定义将受到保护的接口以及有权访问每个接口的角色。在我们的例子中/api/v1/auth/*,接口将是公共的,/api/v1/books接口将受到保护,并且只有具有该角色的用户ADMIN才能访问它。其他接口将受到保护,只有经过身份验证的用户才能访问它。

在该addFilterBefore方法中,我们定义之前创建的过滤器。最后,我们定义了使身份验证系统正常工作所需的AuthenticationManager和beans。PasswordEncoder

授权 DTO

我们需要两个 DTO 来接收用户凭据,并需要另一个 DTO 在用户登录时返回令牌。

// dtos/SignUpDto.java
public record SignUpDto(
    String login,
    String password,
    UserRole role) {
}
// dtos/SignInDto.java
public record SignInDto(
    String login,
    String password) {
}
// dtos/JwtDto.java
public record JwtDto(
    String accessToken) {
}

认证服务

这里我们定义服务实现UserDetailsService,它将负责创建用户并将其保存在数据库中或通过用户名加载用户信息。

// services/AuthService.java
@Service
public class AuthService implements UserDetailsService {

  @Autowired
  UserRepository repository;

  @Override
  public UserDetails loadUserByUsername(String username) {
    var user = repository.findByLogin(username);
    return user;
  }

  public UserDetails signUp(SignUpDto data) throws InvalidJwtException {
    if (repository.findByLogin(data.login()) != null) {
      throw new InvalidJwtException("Username already exists");
    }
    String encryptedPassword = new BCryptPasswordEncoder().encode(data.password());
    User newUser = new User(data.login(), encryptedPassword, data.role());
    return repository.save(newUser);
  }
}

在该signUp方法中,我们检查用户名是否已注册,然后使用 加密密码BCryptPasswordEncoder并保存用户信息。

认证Controller

最后我们定义身份验证控制器。它将负责接收请求、验证用户身份并生成令牌。

// controllers/AuthController.java
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
  @Autowired
  private AuthenticationManager authenticationManager;
  @Autowired
  private AuthService service;
  @Autowired
  private TokenProvider tokenService;

  @PostMapping("/signup")
  public ResponseEntity<?> signUp(@RequestBody @Valid SignUpDto data) {
    service.signUp(data);
    return ResponseEntity.status(HttpStatus.CREATED).build();
  }

  @PostMapping("/signin")
  public ResponseEntity<JwtDto> signIn(@RequestBody @Valid SignInDto data) {
    var usernamePassword = new UsernamePasswordAuthenticationToken(data.login(), data.password());
    var authUser = authenticationManager.authenticate(usernamePassword);
    var accessToken = tokenService.generateAccessToken((User) authUser.getPrincipal());
    return ResponseEntity.ok(new JwtDto(accessToken));
  }
}

在该signUp方法中我们接收用户数据,创建一个新用户并将其保存在数据库中。在该signIn方法中,我们接收用户凭据,使用 验证用户身份AuthenticationManager,并生成令牌。

测试身份验证

要创建新用户,我们POST/api/v1/auth/signup接口发送请求,请求正文包含登录名、密码和可用角色之一(USER 或 ADMIN):

{
  "login": "myusername",
  "password": "123456",
  "role": "USER"
}

为了检索身份验证令牌,我们POST使用此用户登录名和密码向/api/v1/auth/signin接口发送请求。

为了测试我们的身份验证系统,我们将创建一个带有两个接口的简单书籍控制器,一个用于创建一本Book,另一个用于列出所有Book。

@RestController
@RequestMapping("/api/v1/books")
public class BookController {

  @GetMapping
  public ResponseEntity<List<String>> findAll() {
    return ResponseEntity.ok(List.of("Book1", "Book2", "Book3"));
  }

  @PostMapping
  public ResponseEntity<String> create(@RequestBody String data) {
    return ResponseEntity.ok(data);
  }
} 

/api/v1/books接口中,该GET方法将对具有角色的用户可用USER,并且该POST方法将受到保护,并且只有具有该角色的用户ADMIN才能创建书籍。

以上就是使用SpringBoot进行身份验证和授权的示例详解的详细内容,更多关于SpringBoot身份验证授权的资料请关注脚本之家其它相关文章!

相关文章

  • Spring Framework常用面试题及答案汇总

    Spring Framework常用面试题及答案汇总

    这篇文章主要介绍了Spring Framework常用面试题及答案汇总,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-06-06
  • Java如何替换RequestBody和RequestParam参数的属性

    Java如何替换RequestBody和RequestParam参数的属性

    近期由于接手的老项目中存在所有接口中新增一个加密串来给接口做一个加密效果,所以就研究了一下Http请求链路,发现可以通过 javax.servlet.Filter去实现,这篇文章主要介绍了Java替换RequestBody和RequestParam参数的属性,需要的朋友可以参考下
    2023-10-10
  • Spring boot2+jpa+thymeleaf实现增删改查

    Spring boot2+jpa+thymeleaf实现增删改查

    这篇文章主要介绍了Spring boot2+jpa+thymeleaf实现增删改查,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-04-04
  • Java中bcrypt算法实现密码加密的方法步骤

    Java中bcrypt算法实现密码加密的方法步骤

    我们可以在Spring Boot和SSM中实现密码加密,使用bcrypt算法可以保障密码的安全性,并且减少了手动编写哈希函数的工作量,本文就来详细的介绍一下,感兴趣的可以了解一下
    2023-08-08
  • IDEA安装后找不到.vmoptions文件的问题及解决

    IDEA安装后找不到.vmoptions文件的问题及解决

    这篇文章主要介绍了IDEA安装后找不到.vmoptions文件的问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04
  • java遍历读取整个redis数据库实例

    java遍历读取整个redis数据库实例

    这篇文章主要介绍了java遍历读取整个redis数据库实例,使用支持正则表达式的key搜索方法jedis.keys(“*”)实现,需要的朋友可以参考下
    2014-05-05
  • 在Web项目中手机短信验证码实现的全过程记录

    在Web项目中手机短信验证码实现的全过程记录

    这篇文章主要给大家介绍了关于在Web项目中实现短信验证码的全过程记录,文中通过示例代码介绍的非常详细,在文末跟大家提供了源码下载,需要的朋友可以参考借鉴,下面随着小编来一起学习学习吧。
    2017-12-12
  • Java避免死锁_动力节点Java学院整理

    Java避免死锁_动力节点Java学院整理

    在有些情况下死锁是可以避免的。本文将展示三种用于避免死锁的技术。对java避免死锁的相关知识感兴趣的朋友一起通过本文学习吧
    2017-06-06
  • Java日期时间类及计算详解

    Java日期时间类及计算详解

    这篇文章主要介绍了Java日期时间类及计算详解,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下,希望对你的学习有所帮助
    2022-07-07
  • Java实现微信小程序加密数据解密算法

    Java实现微信小程序加密数据解密算法

    我们开发微信小程序的过程中,我们的服务端有时需要获取微信提供的开放数据。微信会对这些开放数据做签名和加密处理,本文通过实例代码给大家介绍Java实现微信小程序加密数据解密算法,感兴趣的朋友一起看看吧
    2021-11-11

最新评论