当前位置:   article > 正文

一文带你了解springboot3+jwt+security的使用_jwt security

jwt security

Spring Security已经成为java后台权限校验的第一选择.今天就通过读代码的方式带大家深入了解一下Security,本文主要是基于开源项目spring-boot-3-jwt-security[1]来讲解Spring Security + JWT(Json Web Token).实现用户鉴权,以及权限校验. 所有代码基于jdk17+构建.现在让我们开始吧!

  1. Springboot 3.0

  2. Spring Security

  3. Json Web Token(JWT)

  4. BCrypt

  5. Maven

  6. 项目使用postgresql数据库来存储用户信息以及Token(为啥不用Redis?这个先挖个坑),可以按照自己的想法替换成mysql数据库

  7. 访问数据库使用的是jpa,对于一些简单的sql可以根据方法名自动映射,还是很方便的.没使用过的也没关系.不影响阅读今天的文章,后续可以根据自己的实际需求替换成mybatis-lpus

  8. 本文使用了Lombok来生成固定的模版代码

  1. <parent>  
  2.     <groupId>org.springframework.boot</groupId>  
  3.     <artifactId>spring-boot-starter-parent</artifactId>  
  4.     <version>3.0.5</version>  
  5.     <relativePath/>   
  6. </parent>  
  7. <groupId>com.alibou</groupId>  
  8. <artifactId>security</artifactId>  
  9. <version>0.0.1-SNAPSHOT</version>  
  10. <name>security</name>  
  11. <description>Demo project for Spring Boot</description>  
  12. <properties>  
  13.     <java.version>17</java.version>  
  14. </properties>  
  15. <dependencies>  
  16.     
  17.     <dependency>  
  18.         <groupId>org.springframework.boot</groupId>  
  19.         <artifactId>spring-boot-starter-data-jpa</artifactId>  
  20.     </dependency>  
  21.     
  22.     <dependency>          
  23.         <groupId>org.springframework.boot</groupId>  
  24.         <artifactId>spring-boot-starter-security</artifactId>  
  25.     </dependency>  
  26.     
  27.     <dependency> 
  28.         <groupId>org.springframework.boot</groupId>  
  29.         <artifactId>spring-boot-starter-web</artifactId>  
  30.     </dependency>  
  31.     
  32.     <dependency>  
  33.         <groupId>org.postgresql</groupId>  
  34.         <artifactId>postgresql</artifactId>  
  35.         <scope>runtime</scope>  
  36.     </dependency>
  37.     
  38.     <dependency>  
  39.         <groupId>org.projectlombok</groupId>  
  40.         <artifactId>lombok</artifactId>  
  41.         <optional>true</optional>  
  42.     </dependency> 
  43.     
  44.     <dependency>  
  45.         <groupId>io.jsonwebtoken</groupId>  
  46.         <artifactId>jjwt-api</artifactId>  
  47.         <version>0.11.5</version>  
  48.     </dependency>  
  49.     <dependency>  
  50.         <groupId>io.jsonwebtoken</groupId>  
  51.         <artifactId>jjwt-impl</artifactId>  
  52.         <version>0.11.5</version>  
  53.     </dependency>  
  54.     <dependency>  
  55.         <groupId>io.jsonwebtoken</groupId>  
  56.         <artifactId>jjwt-jackson</artifactId>  
  57.         <version>0.11.5</version>  
  58.     </dependency>
  59.     
  60.     
  61.     <dependency>  
  62.         <groupId>org.springdoc</groupId>  
  63.         <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>  
  64.         <version>2.1.0</version>  
  65.     </dependency>
  66.     
  67.     <dependency>  
  68.         <groupId>org.springframework.boot</groupId>  
  69.         <artifactId>spring-boot-starter-validation</artifactId>  
  70.     </dependency>  
  71.     <dependency>  
  72.         <groupId>org.springframework.boot</groupId>  
  73.         <artifactId>spring-boot-starter-test</artifactId>  
  74.         <scope>test</scope>  
  75.     </dependency>  
  76.     <dependency>  
  77.         <groupId>org.springframework.security</groupId>  
  78.         <artifactId>spring-security-test</artifactId>  
  79.         <scope>test</scope>  
  80.     </dependency>  
  81. </dependencies>  
  82.   
  83. <build>  
  84.     <plugins>  
  85.         <plugin>  
  86.             <groupId>org.springframework.boot</groupId>  
  87.             <artifactId>spring-boot-maven-plugin</artifactId>  
  88.             <configuration>  
  89.                 <excludes>  
  90.                     <exclude>  
  91.                         <groupId>org.projectlombok</groupId>  
  92.                         <artifactId>lombok</artifactId>  
  93.                     </exclude>  
  94.                 </excludes>  
  95.             </configuration>  
  96.         </plugin>  
  97.     </plugins>  
  98. </build>

鉴权配置

  1. 当项目引入Security依赖后,启动项目会生成一个随机的密码,当我们要访问资源的时候需要使用这个密码登录后才能使用.这会影响我们很多功能的正常使用,比如万恶的swagger.下面我们来详细了解如何配置我们需要鉴权的路径,以及需要放行的路径

  1. @Configuration  
  2. @EnableWebSecurity  
  3. @RequiredArgsConstructor  
  4. @EnableMethodSecurity  
  5. public class SecurityConfiguration {  
  6.   
  7. private final JwtAuthenticationFilter jwtAuthFilter;  
  8. private final AuthenticationProvider authenticationProvider;  
  9. private final LogoutHandler logoutHandler;  
  10.   
  11. @Bean  
  12. public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {  
  13.     http  
  14.     .csrf()  
  15.     .disable() 
  16.     .authorizeHttpRequests()  
  17.     
  18.     .requestMatchers(  
  19.     "/api/v1/auth/**",  
  20.     "/v2/api-docs",  
  21.     "/v3/api-docs",  
  22.     "/v3/api-docs/**",  
  23.     "/swagger-resources",  
  24.     "/swagger-resources/**",  
  25.     "/configuration/ui",  
  26.     "/configuration/security",  
  27.     "/swagger-ui/**",  
  28.     "/webjars/**",  
  29.     "/swagger-ui.html"  
  30.     )  
  31.     .permitAll() 
  32.   
  33.   
  34.     
  35.     * 权限校验(需要登录的用户有指定的权限才可以)  
  36.     * requestMatchers: 指定需要拦截的路径  
  37.     * hasAnyAuthority: 指定需要的权限  
  38.     */  
  39.     .requestMatchers("/api/v1/management/**").hasAnyRole(ADMIN.name(), MANAGER.name())  
  40.     .requestMatchers(GET, "/api/v1/management/**").hasAnyAuthority(ADMIN_READ.name(), MANAGER_READ.name())  
  41.     .requestMatchers(POST, "/api/v1/management/**").hasAnyAuthority(ADMIN_CREATE.name(), MANAGER_CREATE.name())  
  42.     .requestMatchers(PUT, "/api/v1/management/**").hasAnyAuthority(ADMIN_UPDATE.name(), MANAGER_UPDATE.name())  
  43.     .requestMatchers(DELETE, "/api/v1/management/**").hasAnyAuthority(ADMIN_DELETE.name(), MANAGER_DELETE.name())  
  44.     .anyRequest()  
  45.     .authenticated() 
  46.     .and()  
  47.     .sessionManagement()  
  48.     .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 
  49.     .and()  
  50.     .authenticationProvider(authenticationProvider)  
  51.     
  52.     .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)  
  53.     
  54.     .logout()  
  55.     .logoutUrl("/api/v1/auth/logout")  
  56.     .addLogoutHandler(logoutHandler)  
  57.     .logoutSuccessHandler((request, response,authentication) -> SecurityContextHolder.clearContext())  
  58.     ;  
  59.     return http.build();  
  60.     }  
  61. }
  1. 上述代码主要实现了四块功能分别是:
    • 放行不需要鉴权的路径(注册&登录,swagger)

    • 配置访问特定的接口用户需要的权限.(比如想要删除用户必须要有删除用户的权限)

    • 添加前置过滤器,用来从Token中判断用户是否合法和获取用户权限: jwtAuthFilter

    • 配置退出登录的Handler,以及监听的路径.当访问这个路径的时候会自动调用logoutHandler中的方法

登录配置

上面说到了权限和token校验,我们先来了解一下登录的逻辑是什么样的.在security中需要一个UserDetails类来定义用户账户的行为.这个是用户鉴权的关键.主要有账户,密码,权限,用户状态等等.在下面代码中有详细的注释

  1. @Data  
  2. @Builder  
  3. @NoArgsConstructor  
  4. @AllArgsConstructor  
  5. @Entity  
  6. @Table(name = "_user")  
  7. public class User implements UserDetails {  
  8.   
  9.     @Id  
  10.     @GeneratedValue  
  11.     private Integer id; 
  12.     private String firstname; 
  13.     private String lastname; 
  14.     private String email; 
  15.     private String password; 
  16.     
  17.     * 角色枚举  
  18.     */  
  19.     @Enumerated(EnumType.STRING)  
  20.     private Role role;  
  21.     
  22.     * 用户关联的Token  
  23.     * 这里面使用了jpa的一对多映射  
  24.     */  
  25.     @OneToMany(mappedBy = "user")  
  26.     private List<Token> tokens;  
  27.     
  28.     * 获取用户的权限  
  29.     * 这里是根据角色枚举的权限来获取的(静态的而非从数据库动态读取)  
  30.     * @return 用户权限列表  
  31.     */  
  32.     @Override  
  33.     public Collection<? extends GrantedAuthoritygetAuthorities() {  
  34.         return role.getAuthorities();  
  35.     }  
  36.     
  37.     * 获取用户密码  
  38.     * 主要是用来指定你的password字段  
  39.     * @return 用户密码  
  40.     */  
  41.     @Override  
  42.     public String getPassword() {  
  43.         return password;  
  44.     }  
  45.     
  46.     * 获取用户账号  
  47.     * 这里使用email做为账号  
  48.     * @return 用户账号  
  49.     */  
  50.     @Override  
  51.     public String getUsername() {  
  52.         return email;  
  53.     }  
  54.     
  55.     * 账号是否未过期,下面的这个几个方法都是用来指定账号的状态的,因为该项目是一个Demo,所以这里全部返回true  
  56.     * @return true 未过期  
  57.     */  
  58.     @Override  
  59.     public boolean isAccountNonExpired() {  
  60.         return true;  
  61.     }  
  62.     
  63.     * 账号是否未锁定  
  64.     * @return true 未锁定  
  65.     */  
  66.     @Override  
  67.     public boolean isAccountNonLocked() {  
  68.         return true;  
  69.     }  
  70.     
  71.     * 密码是否未过期  
  72.     * @return true 未过期  
  73.     */  
  74.     @Override  
  75.     public boolean isCredentialsNonExpired() {  
  76.         return true;  
  77.     }  
  78.     
  79.     * 账号是否激活  
  80.     * @return true 已激活  
  81.     */  
  82.     @Override  
  83.     public boolean isEnabled() {  
  84.         return true;  
  85.     }  
  86. }

在了解用户实体之后,我们来看一下是怎么来进行登录配置的.如何使用securty来帮我们管理用户密码的校验.下面我们来看一下security的整体配置

  1. @Configuration  
  2. @RequiredArgsConstructor  
  3. public class ApplicationConfig {  
  4.   
  5.     
  6.     * 访问用户数据表  
  7.     */  
  8.     private final UserRepository repository;  
  9.     
  10.     * 获取用户详情Bean  
  11.     * 根据email查询是否存在用户,如果不存在throw用户未找到异常  
  12.     */  
  13.     @Bean  
  14.     public UserDetailsService userDetailsService() {  
  15.         
  16.         return username -> repository.findByEmail(username)  
  17.         
  18.         .orElseThrow(() -> new UsernameNotFoundException("User not found"));  
  19.     }  
  20.     
  21.     * 身份验证Bean  
  22.     * 传入获取用户信息的bean & 密码加密器  
  23.     * 可以回看一下SecurityConfiguration中 AuthenticationProvider的配置,使用的就是这里注入到容器中的Bean  
  24.     * 这个bean 主要是用于用户登录时的身份验证,当我们登录的时候security会帮我们调用这个bean的authenticate方法  
  25.     */  
  26.     @Bean  
  27.     public AuthenticationProvider authenticationProvider() {  
  28.         DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();  
  29.         
  30.         authProvider.setUserDetailsService(userDetailsService());  
  31.         
  32.         authProvider.setPasswordEncoder(passwordEncoder());  
  33.         return authProvider;  
  34.     }  
  35.     
  36.     * 身份验证管理器  
  37.     */  
  38.     @Bean  
  39.     public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {  
  40.         return config.getAuthenticationManager();  
  41.     }  
  42.     
  43.     * 密码加密器  
  44.     * 主要是用来指定数据库中存储密码的加密方式,保证密码非明文存储  
  45.     * 当security需要进行密码校验时,会把请求传进来的密码进行加密,然后和数据库中的密码进行比对  
  46.     */  
  47.     @Bean  
  48.     public PasswordEncoder passwordEncoder() {  
  49.         return new BCryptPasswordEncoder();  
  50.     }  
  51.   
  52. }

上述代码主要做了两件事:

  • 指定我们如何从数据库中根据用户账号获取用户信息

  • 指定用户密码的加密器passwordEncoder

现在大家可能会存在一个疑问,security怎么知道User实体中那个字段是我的账户,那个字段是我的密码? 不知道大家是否记得UserDetails类,也就是我们的User类.其中有两个方法getPassword & getUsername.这两个方法返回的就是账号和密码.User类中的还有几个其他的方法,可以根据我们实际的业务需求来对账号进行禁用等操作.

Token如何生成

token的生成主要是使用工具包来实现,在本项目中Token中主要存储用户信息 & 用户权限,下面我们先看一下token工具包的代码.主要包括为: 生成token,从token中获取信息,以及验证token

  1. @Service  
  2. public class JwtService {  
  3.   
  4.     
  5.     * 加密盐值  
  6.     */  
  7.     @Value("${application.security.jwt.secret-key}")  
  8.     private String secretKey;  
  9.     
  10.     * Token失效时间  
  11.     */  
  12.     @Value("${application.security.jwt.expiration}")  
  13.     private long jwtExpiration;  
  14.     
  15.     * Token刷新时间  
  16.     */  
  17.     @Value("${application.security.jwt.refresh-token.expiration}")  
  18.     private long refreshExpiration;  
  19.     
  20.     * 从Token中获取Username  
  21.     * @param token Token  
  22.     * @return String  
  23.     */  
  24.     public String extractUsername(String token) {  
  25.         return extractClaim(token, Claims::getSubject);  
  26.     }  
  27.     
  28.     * 从Token中回去数据,根据传入不同的Function返回不同的数据  
  29.     * eg: String extractUsername(String token)  
  30.     */  
  31.     public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {  
  32.         final Claims claims = extractAllClaims(token);  
  33.         return claimsResolver.apply(claims);  
  34.     }  
  35.     
  36.     * 生成Token无额外信息  
  37.     */  
  38.     public String generateToken(UserDetails userDetails) {  
  39.         return generateToken(new HashMap<>(), userDetails);  
  40.     }  
  41.     
  42.     * 生成Token,有额外信息  
  43.     * @param extraClaims 额外的数据  
  44.     * @param userDetails 用户信息  
  45.     * @return String  
  46.     */  
  47.     public String generateToken(  
  48.     Map<StringObject> extraClaims,  
  49.     UserDetails userDetails  
  50.     ) {  
  51.         return buildToken(extraClaims, userDetails, jwtExpiration);  
  52.     }  
  53.     
  54.     * 生成刷新用的Token  
  55.     * @param userDetails 用户信息  
  56.     * @return String  
  57.     */  
  58.     public String generateRefreshToken(  
  59.     UserDetails userDetails  
  60.     ) {  
  61.         return buildToken(new HashMap<>(), userDetails, refreshExpiration);  
  62.     }  
  63.     
  64.     * 构建Token方法  
  65.     * @param extraClaims 额外信息  
  66.     * @param userDetails //用户信息  
  67.     * @param expiration //失效时间  
  68.     * @return String  
  69.     */  
  70.     private String buildToken(  
  71.         Map<StringObject> extraClaims,  
  72.         UserDetails userDetails,  
  73.         long expiration  
  74.         ) {  
  75.         return Jwts  
  76.         .builder()  
  77.         .setClaims(extraClaims) 
  78.         .setSubject(userDetails.getUsername()) 
  79.         .setIssuedAt(new Date(System.currentTimeMillis())) 
  80.         .setExpiration(new Date(System.currentTimeMillis() + expiration)) 
  81.         .signWith(getSignInKey(), SignatureAlgorithm.HS256
  82.         .compact();  
  83.     }  
  84.     
  85.     * 验证Token是否有效  
  86.     * @param token Token  
  87.     * @param userDetails 用户信息  
  88.     * @return boolean  
  89.     */  
  90.     public boolean isTokenValid(String token, UserDetails userDetails) {  
  91.         final String username = extractUsername(token);  
  92.         return (username.equals(userDetails.getUsername())) && !isTokenExpired(token); 
  93.     }  
  94.     
  95.     * 判断Token是否过去  
  96.     */  
  97.     private boolean isTokenExpired(String token) {  
  98.         return extractExpiration(token).before(new Date());  
  99.     }  
  100.     
  101.     * 从Token中获取失效时间  
  102.     */  
  103.     private Date extractExpiration(String token) {  
  104.         
  105.         return extractClaim(token, Claims::getExpiration);  
  106.     }  
  107.     
  108.     * 从Token中获取所有数据  
  109.     */  
  110.     private Claims extractAllClaims(String token) {  
  111.         return Jwts  
  112.         .parserBuilder()  
  113.         .setSigningKey(getSignInKey())  
  114.         .build()  
  115.         .parseClaimsJws(token)  
  116.         .getBody();  
  117.     }  
  118.     
  119.     * 获取签名Key  
  120.     * Token 加密解密使用  
  121.     */  
  122.     private Key getSignInKey() {  
  123.         byte[] keyBytes = Decoders.BASE64.decode(secretKey);  
  124.         return Keys.hmacShaKeyFor(keyBytes);  
  125.     }  
  126. }

注册和登录

token的生成已经看过了,下面该进入最关键的环节了.用户注册 & 用户登录

  1. 用户注册: 接收到用户传递过来的信息,在数据库中生成用户信息(密码会通过passwordEncoder进行加密).用户信息保存成功后,会根据用户信息创建一个鉴权token和一个refreshToken

  2. 用户登录: 获取到用户传递的账号密码后,会创建一个UsernamePasswordAuthenticationToken对象.然后通过authenticationManagerauthenticate方法进行校验,如果出现错误会根据错误的不同抛出不同的异常.在实际开发中可以通过捕获的异常类型不同来创建响应提示.

  1. @RestController  
  2. @RequestMapping("/api/v1/auth")  
  3. @RequiredArgsConstructor  
  4. public class AuthenticationController {  
  5.     private final AuthenticationService service;  
  6.     
  7.     * 注册方法  
  8.     * @param request 请求体  
  9.     * @return ResponseEntity  
  10.     */  
  11.     @PostMapping("/register")  
  12.     public ResponseEntity<AuthenticationResponse> register(  
  13.     @RequestBody RegisterRequest request  
  14.     ) {  
  15.         return ResponseEntity.ok(service.register(request));  
  16.     }  
  17.     
  18.     * 鉴权(登录方法)  
  19.     * @param request 请求体  
  20.     * @return ResponseEntity  
  21.     */  
  22.     @PostMapping("/authenticate")  
  23.     public ResponseEntity<AuthenticationResponse> authenticate(  
  24.     @RequestBody AuthenticationRequest request  
  25.     ) {  
  26.         return ResponseEntity.ok(service.authenticate(request));  
  27.     }  
  28.     
  29.     * 刷新token  
  30.     * @param request 请求体  
  31.     * @param response 响应体  
  32.     * @throws IOException 异常  
  33.     */  
  34.     @PostMapping("/refresh-token")  
  35.     public void refreshToken(  
  36.     HttpServletRequest request,  
  37.     HttpServletResponse response  
  38.     ) throws IOException {  
  39.         service.refreshToken(request, response);  
  40.     }  
  41. }

可以看出来controller中的方法都是对service方法的调用,我们现在看一下service中的代码

  1. @Service  
  2. @RequiredArgsConstructor  
  3. public class AuthenticationService {  
  4.   
  5.     private final UserRepository repository
  6.     private final TokenRepository tokenRepository; 
  7.     private final PasswordEncoder passwordEncoder; 
  8.     private final JwtService jwtService; 
  9.     private final AuthenticationManager authenticationManager; 
  10.     
  11.     * 注册方法  
  12.     * @param request 请求体  
  13.     * @return AuthenticationResponse(自己封装的响应结构)  
  14.     */  
  15.     public AuthenticationResponse register(RegisterRequest request) {  
  16.     
  17.         var user = User.builder()  
  18.         .firstname(request.getFirstname())  
  19.         .lastname(request.getLastname())  
  20.         .email(request.getEmail())  
  21.         .password(passwordEncoder.encode(request.getPassword()))  
  22.         .role(request.getRole())  
  23.         .build();  
  24.         
  25.         var savedUser = repository.save(user);  
  26.         
  27.         var jwtToken = jwtService.generateToken(user);  
  28.         
  29.         var refreshToken = jwtService.generateRefreshToken(user);  
  30.         
  31.         saveUserToken(savedUser, jwtToken);  
  32.         
  33.         return AuthenticationResponse.builder()  
  34.         .accessToken(jwtToken)  
  35.         .refreshToken(refreshToken)  
  36.         .build();  
  37.     }  
  38.     
  39.     * 鉴权(登录)方法  
  40.     * @param request 请求体  
  41.     * @return AuthenticationResponse(自己封装的响应结构)  
  42.     */  
  43.     public AuthenticationResponse authenticate(AuthenticationRequest request) {  
  44.         
  45.         
  46.         authenticationManager.authenticate(  
  47.         new UsernamePasswordAuthenticationToken(  
  48.         request.getEmail(),  
  49.         request.getPassword()  
  50.         )  
  51.         );  
  52.         
  53.         var user = repository.findByEmail(request.getEmail())  
  54.         .orElseThrow();  
  55.         
  56.         var jwtToken = jwtService.generateToken(user);  
  57.         
  58.         var refreshToken = jwtService.generateRefreshToken(user);  
  59.         
  60.         revokeAllUserTokens(user);  
  61.         
  62.         saveUserToken(user, jwtToken);  
  63.         
  64.         return AuthenticationResponse.builder()  
  65.         .accessToken(jwtToken)  
  66.         .refreshToken(refreshToken)  
  67.         .build();  
  68.     }  
  69.     
  70.     * 保存用户Token方法  
  71.     * 构建Token实体后保存到数据库  
  72.     * @param user 用户信息  
  73.     * @param jwtToken Token  
  74.     */  
  75.     private void saveUserToken(User user, String jwtToken) {  
  76.         var token = Token.builder()  
  77.         .user(user)  
  78.         .token(jwtToken)  
  79.         .tokenType(TokenType.BEARER)  
  80.         .expired(false)  
  81.         .revoked(false)  
  82.         .build();  
  83.         tokenRepository.save(token);  
  84.     }  
  85.     
  86.     * 将用户所有Token变成失效状态  
  87.     * @param user 用户信息  
  88.     */  
  89.     private void revokeAllUserTokens(User user) {  
  90.         
  91.         var validUserTokens = tokenRepository.findAllValidTokenByUser(user.getId());  
  92.         if (validUserTokens.isEmpty()){  
  93.         return;  
  94.         }  
  95.         
  96.         validUserTokens.forEach(token -> {  
  97.         token.setExpired(true);  
  98.         token.setRevoked(true);  
  99.         });  
  100.         tokenRepository.saveAll(validUserTokens);  
  101.     }  
  102.     
  103.     * 刷新token方法  
  104.     * @param request 请求体  
  105.     * @param response 响应体  
  106.     * @throws IOException 抛出IO异常  
  107.     */  
  108.     public void refreshToken(  
  109.     HttpServletRequest request,  
  110.     HttpServletResponse response  
  111.     ) throws IOException {  
  112.         
  113.         final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);  
  114.         final String refreshToken;  
  115.         final String userEmail;  
  116.         
  117.         if (authHeader == null ||!authHeader.startsWith("Bearer ")) {  
  118.             return;  
  119.         }  
  120.         
  121.         refreshToken = authHeader.substring(7);  
  122.         
  123.         userEmail = jwtService.extractUsername(refreshToken);  
  124.         if (userEmail != null) {  
  125.             
  126.             var user = this.repository.findByEmail(userEmail)  
  127.             .orElseThrow();  
  128.             
  129.             if (jwtService.isTokenValid(refreshToken, user)) {  
  130.                 
  131.                 var accessToken = jwtService.generateToken(user);  
  132.                 revokeAllUserTokens(user);  
  133.                 saveUserToken(user, accessToken);  
  134.                 
  135.                 var authResponse = AuthenticationResponse.builder()  
  136.                 .accessToken(accessToken)  
  137.                 .refreshToken(refreshToken)  
  138.                 .build();  
  139.                 new ObjectMapper().writeValue(response.getOutputStream(), authResponse);  
  140.             }  
  141.         }  
  142.     }  
  143. }

上述代码主要说明了,注册 & 登录后返回token的流程,当前项目中由于token & refreshToken有效期较长所以选择了将token保存到数据库(个人观点!!!).可以根据自己业务的实际需求来决定是否需要保存到redis

请求过滤

请求过滤主要是在每次请求的时候动态解析token来获取用户信息以及权限,来保证请求资源的安全性.防止越权访问等.

  1. @Component  
  2. @RequiredArgsConstructor  
  3. public class JwtAuthenticationFilter extends OncePerRequestFilter {  
  4.   
  5.     private final JwtService jwtService;  
  6.     private final UserDetailsService userDetailsService;  
  7.     private final TokenRepository tokenRepository;  
  8.     @Override  
  9.     protected void doFilterInternal(  
  10.     @NonNull HttpServletRequest request,  
  11.     @NonNull HttpServletResponse response,  
  12.     @NonNull FilterChain filterChain  
  13.     ) throws ServletException, IOException {  
  14.         
  15.         if (request.getServletPath().contains("/api/v1/auth")) {  
  16.             filterChain.doFilter(request, response);  
  17.             return;  
  18.         }  
  19.         
  20.         final String authHeader = request.getHeader("Authorization");  
  21.         final String jwt;  
  22.         final String userEmail;  
  23.         
  24.         if (authHeader == null ||!authHeader.startsWith("Bearer ")) {  
  25.             filterChain.doFilter(request, response);  
  26.             return;  
  27.         }  
  28.         
  29.         jwt = authHeader.substring(7);  
  30.         
  31.         userEmail = jwtService.extractUsername(jwt);  
  32.         
  33.         if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {  
  34.             
  35.             UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);  
  36.             
  37.             var isTokenValid = tokenRepository.findByToken(jwt)  
  38.                 .map(t -> !t.isExpired() && !t.isRevoked())  
  39.                 .orElse(false);  
  40.             
  41.             if (jwtService.isTokenValid(jwt, userDetails) && isTokenValid) {  
  42.                 UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(  
  43.                 userDetails, 
  44.                 null,  
  45.                 userDetails.getAuthorities() 
  46.                 );  
  47.                 authToken.setDetails(  
  48.                     new WebAuthenticationDetailsSource().buildDetails(request) 
  49.                 );  
  50.                 
  51.                 
  52.                 SecurityContextHolder.getContext().setAuthentication(authToken);  
  53.             }  
  54.         }  
  55.         filterChain.doFilter(request, response);  
  56.     }  
  57. }

上述代码主要逻辑为: 从请求头中获取到token.验证token的有效性并解析token中的信息存储到SecurityContextHolder上下文中,方便后续的使用.

退出登录

登录以及token的校验已经说过了,现在就差一个退出登录了.大家是否还记得我们之前配置过一个退出登录的请求路径: /api/v1/auth/logout.当我们请求请求这个路径的时候,security会帮我们找到对应的LogoutHandler,然后调用logout方法实现退出登录.

  1. @Service  
  2. @RequiredArgsConstructor  
  3. public class LogoutService implements LogoutHandler {  
  4.     private final TokenRepository tokenRepository;  
  5.     @Override  
  6.     public void logout(  
  7.     HttpServletRequest request,  
  8.     HttpServletResponse response,  
  9.     Authentication authentication  
  10.     ) {  
  11.         
  12.         final String authHeader = request.getHeader("Authorization");  
  13.         final String jwt;  
  14.         if (authHeader == null ||!authHeader.startsWith("Bearer ")) {  
  15.         return;  
  16.         }  
  17.         
  18.         jwt = authHeader.substring(7);  
  19.         
  20.         var storedToken = tokenRepository.findByToken(jwt)  
  21.         .orElse(null);  
  22.         if (storedToken != null) {  
  23.             
  24.             storedToken.setExpired(true);  
  25.             storedToken.setRevoked(true);  
  26.             tokenRepository.save(storedToken);  
  27.             
  28.             SecurityContextHolder.clearContext();  
  29.         }  
  30.     }  
  31. }

security帮我们做了很多的事情,我们只需要把token置为失效状态,然后清除掉SecurityContextHolder上下文,就解决了全部的问题

下面通过几个例子,来讲解两种不同的鉴权配置方式

controller

  1. @RestController  
  2. @RequestMapping("/api/v1/admin")  
  3. @PreAuthorize("hasRole('ADMIN')"
  4. public class AdminController {  
  5.   
  6.     @GetMapping  
  7.     @PreAuthorize("hasAuthority('admin:read')"
  8.     public String get() {  
  9.         return "GET:: admin controller";  
  10.     }  
  11.     @PostMapping  
  12.     @PreAuthorize("hasAuthority('admin:create')"
  13.     @Hidden  
  14.     public String post() {  
  15.         return "POST:: admin controller";  
  16.     }  
  17.     @PutMapping  
  18.     @PreAuthorize("hasAuthority('admin:update')")  
  19.     @Hidden  
  20.     public String put() {  
  21.         return "PUT:: admin controller";  
  22.     }  
  23.     @DeleteMapping  
  24.     @PreAuthorize("hasAuthority('admin:delete')")  
  25.     @Hidden  
  26.     public String delete() {  
  27.         return "DELETE:: admin controller";  
  28.     }  
  29. }

配置文件

下面贴出SecurityConfiguration配置类的部分代码

参考资料

[1]

https://github.com/ali-bouali/spring-boot-3-jwt-security: https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fali-bouali%2Fspring-boot-3-jwt-security

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/388647
推荐阅读
相关标签
  

闽ICP备14008679号