Spring Boot 3 + JWT + Security join forces to create a security empire: an article lets you control the future!

Preface

Spring SecurityIt has become javathe first choice for background permission verification. Today I will give you an in-depth understanding of Security by reading the code. This article is mainly based on the open source project spring-boot-3-jwt-security to explain Spring Security + JWT (Json Web Token ). Implement user authentication and permission verification.
All codes are based on jdk17+construction. Now let’s get started!

1 Technical introduction

  1. Springboot 3.0
  2. Spring Security
  3. Json Web Token(JWT)
  4. BCrypt
  5. Maven

2 project construction

  1. The project uses postgresqla database to store user information and (why not use Redis? Let’s dig a hole first). You can replace it with a database Tokenaccording to your own ideas.mysql
  2. When accessing the database jpa, some simple SQL can be automatically mapped according to the method name, which is very convenient. It doesn’t matter if you haven’t used it before. It does not affect reading today’s article. You can replace it with the following according to your actual needs mybatis-lpus.
  3. This article uses Lombok to generate fixed template code
<parent>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-parent</artifactId>  
    <version>3.0.5</version>  
    <relativePath/> <!-- lookup parent from repository -->  
</parent>  
<groupId>com.alibou</groupId>  
<artifactId>security</artifactId>  
<version>0.0.1-SNAPSHOT</version>  
<name>security</name>  
<description>Demo project for Spring Boot</description>  
<properties>  
    <java.version>17</java.version>  
</properties>  
<dependencies>  
    <!-- jpa -->
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-data-jpa</artifactId>  
    </dependency>  
    <!-- spring security 安全框架 -->
    <dependency>          
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-security</artifactId>  
    </dependency>  
    <!-- web 依赖 -->
    <dependency> 
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-web</artifactId>  
    </dependency>  
    <!-- 数据库 -->
    <dependency>  
        <groupId>org.postgresql</groupId>  
        <artifactId>postgresql</artifactId>  
        <scope>runtime</scope>  
    </dependency>
    <!-- lombok -->
    <dependency>  
        <groupId>org.projectlombok</groupId>  
        <artifactId>lombok</artifactId>  
        <optional>true</optional>  
    </dependency> 
    <!-- JWT -->
    <dependency>  
        <groupId>io.jsonwebtoken</groupId>  
        <artifactId>jjwt-api</artifactId>  
        <version>0.11.5</version>  
    </dependency>  
    <dependency>  
        <groupId>io.jsonwebtoken</groupId>  
        <artifactId>jjwt-impl</artifactId>  
        <version>0.11.5</version>  
    </dependency>  
    <dependency>  
        <groupId>io.jsonwebtoken</groupId>  
        <artifactId>jjwt-jackson</artifactId>  
        <version>0.11.5</version>  
    </dependency>
    
    <!-- doc 这个不需要的可以去掉 -->
    <dependency>  
        <groupId>org.springdoc</groupId>  
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>  
        <version>2.1.0</version>  
    </dependency>
    <!-- 校验 -->
    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-validation</artifactId>  
    </dependency>  

    <dependency>  
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-starter-test</artifactId>  
        <scope>test</scope>  
    </dependency>  
    <dependency>  
        <groupId>org.springframework.security</groupId>  
        <artifactId>spring-security-test</artifactId>  
        <scope>test</scope>  
    </dependency>  
</dependencies>  
  
<build>  
    <plugins>  
        <plugin>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-maven-plugin</artifactId>  
            <configuration>  
                <excludes>  
                    <exclude>  
                        <groupId>org.projectlombok</groupId>  
                        <artifactId>lombok</artifactId>  
                    </exclude>  
                </excludes>  
            </configuration>  
        </plugin>  
    </plugins>  
</build>

3 project configuration

3.1 Authentication configuration

  1. When the project introduces Securitydependencies, a random password will be generated when starting the project. When we want to access resources, we need to use this password to log in before we can use it. This will affect the normal use of many of our functions, such as the worst. Let’s learn more about it below swagger. How to configure the paths we need to authenticate and the paths that need to be released
@Configuration  
@EnableWebSecurity  
@RequiredArgsConstructor  
@EnableMethodSecurity  
public class SecurityConfiguration {
    
      
  
private final JwtAuthenticationFilter jwtAuthFilter;  
private final AuthenticationProvider authenticationProvider;  
private final LogoutHandler logoutHandler;  
  
@Bean  
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    
      
    http  
    .csrf()  
    .disable() //关闭csrf(跨域)  
    .authorizeHttpRequests()  
    //配置需要放行的路径  
    .requestMatchers(  
    "/api/v1/auth/**",  
    "/v2/api-docs",  
    "/v3/api-docs",  
    "/v3/api-docs/**",  
    "/swagger-resources",  
    "/swagger-resources/**",  
    "/configuration/ui",  
    "/configuration/security",  
    "/swagger-ui/**",  
    "/webjars/**",  
    "/swagger-ui.html"  
    )  
    .permitAll() //放行上述的所有路径  
  
  
    /*  
    * 权限校验(需要登录的用户有指定的权限才可以)  
    * requestMatchers: 指定需要拦截的路径  
    * hasAnyAuthority: 指定需要的权限  
    */  
    .requestMatchers("/api/v1/management/**").hasAnyRole(ADMIN.name(), MANAGER.name())  
    .requestMatchers(GET, "/api/v1/management/**").hasAnyAuthority(ADMIN_READ.name(), MANAGER_READ.name())  
    .requestMatchers(POST, "/api/v1/management/**").hasAnyAuthority(ADMIN_CREATE.name(), MANAGER_CREATE.name())  
    .requestMatchers(PUT, "/api/v1/management/**").hasAnyAuthority(ADMIN_UPDATE.name(), MANAGER_UPDATE.name())  
    .requestMatchers(DELETE, "/api/v1/management/**").hasAnyAuthority(ADMIN_DELETE.name(), MANAGER_DELETE.name())  
    .anyRequest()  
    .authenticated() //设置所有的请求都需要验证  
    .and()  
    .sessionManagement()  
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //使用无状态Session  
    .and()  
    .authenticationProvider(authenticationProvider)  
    //添加jwt过滤器  
    .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)  
    //设置logout(当调用这个接口的时候, 会调用logoutHandler的logout方法)  
    .logout()  
    .logoutUrl("/api/v1/auth/logout")  
    .addLogoutHandler(logoutHandler)  
    .logoutSuccessHandler((request, response,authentication) -> SecurityContextHolder.clearContext())  
    ;  

    return http.build();  
    }  
}
  1. The above code mainly implements four functions:
    • Release paths that do not require authentication (registration & login, swagger)
    • Configure the permissions required for users to access specific interfaces. (For example, if you want to delete a user, you must have the permission to delete the user)
    • Add a pre-filter to determine whether the user is legal and obtain user permissions from the Token:jwtAuthFilter
    • Configure the Handler to log out and the path to monitor. When accessing this path, logoutHandlerthe method in will be automatically called.

3.2 Login configuration

Permissions and verification are mentioned above token. Let’s first understand what the login logic is. securityA class is needed UserDetailsto define the behavior of the user account. This is the key to user authentication. It mainly includes accounts, passwords, permissions, and users. Status, etc. There are detailed comments in the code below

@Data  
@Builder  
@NoArgsConstructor  
@AllArgsConstructor  
@Entity  
@Table(name = "_user")  
public class User implements UserDetails {
    
      
  
    @Id  
    @GeneratedValue  
    private Integer id; //主键ID  
    private String firstname; //名字  
    private String lastname; //姓氏  
    private String email; //邮箱  
    private String password; //密码  

    /**  
    * 角色枚举  
    */  
    @Enumerated(EnumType.STRING)  
    private Role role;  

    /**  
    * 用户关联的Token  
    * 这里面使用了jpa的一对多映射  
    */  
    @OneToMany(mappedBy = "user")  
    private List<Token> tokens;  

    /**  
    * 获取用户的权限  
    * 这里是根据角色枚举的权限来获取的(静态的而非从数据库动态读取)  
    * @return 用户权限列表  
    */  
    @Override  
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
      
        return role.getAuthorities();  
    }  

    /**  
    * 获取用户密码  
    * 主要是用来指定你的password字段  
    * @return 用户密码  
    */  
    @Override  
    public String getPassword() {
    
      
        return password;  
    }  

    /**  
    * 获取用户账号  
    * 这里使用email做为账号  
    * @return 用户账号  
    */  
    @Override  
    public String getUsername() {
    
      
        return email;  
    }  

    /**  
    * 账号是否未过期,下面的这个几个方法都是用来指定账号的状态的,因为该项目是一个Demo,所以这里全部返回true  
    * @return true 未过期  
    */  
    @Override  
    public boolean isAccountNonExpired() {
    
      
        return true;  
    }  

    /**  
    * 账号是否未锁定  
    * @return true 未锁定  
    */  
    @Override  
    public boolean isAccountNonLocked() {
    
      
        return true;  
    }  

    /**  
    * 密码是否未过期  
    * @return true 未过期  
    */  
    @Override  
    public boolean isCredentialsNonExpired() {
    
      
        return true;  
    }  

    /**  
    * 账号是否激活  
    * @return true 已激活  
    */  
    @Override  
    public boolean isEnabled() {
    
      
        return true;  
    }  
}

After understanding the user entity, let's take a look at how to configure login. How to use it to securtyhelp us manage user password verification. Let's take a look at securitythe overall configuration.

@Configuration  
@RequiredArgsConstructor  
public class ApplicationConfig {
    
      
  
    /**  
    * 访问用户数据表  
    */  
    private final UserRepository repository;  

    /**  
    * 获取用户详情Bean  
    * 根据email查询是否存在用户,如果不存在throw用户未找到异常  
    */  
    @Bean  
    public UserDetailsService userDetailsService() {
    
      
        //调用repository的findByEmail方法,来获取用户信息,如果存在则返回,如果不存在则抛出异常  
        return username -> repository.findByEmail(username)  
        //这里使用的Option的orElseThrow方法,如果存在则返回,如果不存在则抛出异常  
        .orElseThrow(() -> new UsernameNotFoundException("User not found"));  
    }  

    /**  
    * 身份验证Bean  
    * 传入获取用户信息的bean & 密码加密器  
    * 可以回看一下SecurityConfiguration中 AuthenticationProvider的配置,使用的就是这里注入到容器中的Bean  
    * 这个bean 主要是用于用户登录时的身份验证,当我们登录的时候security会帮我们调用这个bean的authenticate方法  
    */  
    @Bean  
    public AuthenticationProvider authenticationProvider() {
    
      
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();  
        //设置获取用户信息的bean  
        authProvider.setUserDetailsService(userDetailsService());  
        //设置密码加密器  
        authProvider.setPasswordEncoder(passwordEncoder());  
        return authProvider;  
    }  

    /**  
    * 身份验证管理器  
    */  
    @Bean  
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
    
      
        return config.getAuthenticationManager();  
    }  

    /**  
    * 密码加密器  
    * 主要是用来指定数据库中存储密码的加密方式,保证密码非明文存储  
    * 当security需要进行密码校验时,会把请求传进来的密码进行加密,然后和数据库中的密码进行比对  
    */  
    @Bean  
    public PasswordEncoder passwordEncoder() {
    
      
        return new BCryptPasswordEncoder();  
    }  
  
}

The above code mainly does two things:

  • Specify how we obtain user information from the database based on user account
  • Encryptor for specifying user passwordpasswordEncoder

Now you may have a question, securityhow do you know Userwhich field in the entity is my account and which field is my password?
I don’t know if you remember UserDetailsthe class, which is our Userclass. There are two methods getPassword& getUsername. These two methods What is returned is the account number and password. UserThere are several other methods in the class that can perform 禁用operations on the account according to our actual business needs.

3.3 How to generate Token

tokenThe generation is mainly achieved using toolkits. In this project, Token mainly stores 用户信息& 用户权限. Let's take a look at tokenthe code of the toolkit first. It mainly includes: generation token, obtaining information tokenfrom it , and verification.token

@Service  
public class JwtService {
    
      
  
    /**  
    * 加密盐值  
    */  
    @Value("${application.security.jwt.secret-key}")  
    private String secretKey;  

    /**  
    * Token失效时间  
    */  
    @Value("${application.security.jwt.expiration}")  
    private long jwtExpiration;  

    /**  
    * Token刷新时间  
    */  
    @Value("${application.security.jwt.refresh-token.expiration}")  
    private long refreshExpiration;  

    /**  
    * 从Token中获取Username  
    * @param token Token  
    * @return String  
    */  
    public String extractUsername(String token) {
    
      
        return extractClaim(token, Claims::getSubject);  
    }  

    /**  
    * 从Token中回去数据,根据传入不同的Function返回不同的数据  
    * eg: String extractUsername(String token)  
    */  
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
    
      
        final Claims claims = extractAllClaims(token);  
        return claimsResolver.apply(claims);  
    }  

    /**  
    * 生成Token无额外信息  
    */  
    public String generateToken(UserDetails userDetails) {
    
      
        return generateToken(new HashMap<>(), userDetails);  
    }  

    /**  
    * 生成Token,有额外信息  
    * @param extraClaims 额外的数据  
    * @param userDetails 用户信息  
    * @return String  
    */  
    public String generateToken(  
    Map<String, Object> extraClaims,  
    UserDetails userDetails  
    ) {
    
      
        return buildToken(extraClaims, userDetails, jwtExpiration);  
    }  

    /**  
    * 生成刷新用的Token  
    * @param userDetails 用户信息  
    * @return String  
    */  
    public String generateRefreshToken(  
    UserDetails userDetails  
    ) {
    
      
        return buildToken(new HashMap<>(), userDetails, refreshExpiration);  
    }  

    /**  
    * 构建Token方法  
    * @param extraClaims 额外信息  
    * @param userDetails //用户信息  
    * @param expiration //失效时间  
    * @return String  
    */  
    private String buildToken(  
        Map<String, Object> extraClaims,  
        UserDetails userDetails,  
        long expiration  
        ) {
    
      
        return Jwts  
        .builder()  
        .setClaims(extraClaims) //body  
        .setSubject(userDetails.getUsername()) //主题数据  
        .setIssuedAt(new Date(System.currentTimeMillis())) //设置发布时间  
        .setExpiration(new Date(System.currentTimeMillis() + expiration)) //设置过期时间  
        .signWith(getSignInKey(), SignatureAlgorithm.HS256) //设置摘要算法  
        .compact();  
    }  

    /**  
    * 验证Token是否有效  
    * @param token Token  
    * @param userDetails 用户信息  
    * @return boolean  
    */  
    public boolean isTokenValid(String token, UserDetails userDetails) {
    
      
        final String username = extractUsername(token);  
        return (username.equals(userDetails.getUsername())) && !isTokenExpired(token); 
    }  

    /**  
    * 判断Token是否过去  
    */  
    private boolean isTokenExpired(String token) {
    
      
        return extractExpiration(token).before(new Date());  
    }  

    /**  
    * 从Token中获取失效时间  
    */  
    private Date extractExpiration(String token) {
    
      
        //通用方法,传入一个Function,返回一个T  
        return extractClaim(token, Claims::getExpiration);  
    }  

    /**  
    * 从Token中获取所有数据  
    */  
    private Claims extractAllClaims(String token) {
    
      
        return Jwts  
        .parserBuilder()  
        .setSigningKey(getSignInKey())  
        .build()  
        .parseClaimsJws(token)  
        .getBody();  
    }  

    /**  
    * 获取签名Key  
    * Token 加密解密使用  
    */  
    private Key getSignInKey() {
    
      
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);  
        return Keys.hmacShaKeyFor(keyBytes);  
    }  
}

3.4 Registration and login

tokenWe have already seen the generation of , now it’s time to enter the most critical link. 用户注册&用户登录

  1. User registration: Receive the information passed by the user and generate the user information in the database (the password will be passwordEncoderencrypted). After the user information is successfully saved, an authentication tokenand an authentication code will be created based on the user information.refreshToken
  2. User login: After obtaining the account password passed by the user, an UsernamePasswordAuthenticationTokenobject will be created. Then it will be verified through authenticationManagerthe authenticatemethod. If an error occurs, different exceptions will be thrown according to the error. In actual development, different exception types can be captured. to create a response prompt.
@RestController  
@RequestMapping("/api/v1/auth")  
@RequiredArgsConstructor  
public class AuthenticationController {
    
      

    private final AuthenticationService service;  

    /**  
    * 注册方法  
    * @param request 请求体  
    * @return ResponseEntity  
    */  
    @PostMapping("/register")  
    public ResponseEntity<AuthenticationResponse> register(  
    @RequestBody RegisterRequest request  
    ) {
    
      
        return ResponseEntity.ok(service.register(request));  
    }  

    /**  
    * 鉴权(登录方法)  
    * @param request 请求体  
    * @return ResponseEntity  
    */  
    @PostMapping("/authenticate")  
    public ResponseEntity<AuthenticationResponse> authenticate(  
    @RequestBody AuthenticationRequest request  
    ) {
    
      
        return ResponseEntity.ok(service.authenticate(request));  
    }  

    /**  
    * 刷新token  
    * @param request 请求体  
    * @param response 响应体  
    * @throws IOException 异常  
    */  
    @PostMapping("/refresh-token")  
    public void refreshToken(  
    HttpServletRequest request,  
    HttpServletResponse response  
    ) throws IOException {
    
      
        service.refreshToken(request, response);  
    }  
}

It can be seen that controllerthe methods in are all servicecalls to methods. Let’s take a look at servicethe code in

@Service  
@RequiredArgsConstructor  
public class AuthenticationService {
    
      
  
    private final UserRepository repository; //访问user数据库  
    private final TokenRepository tokenRepository; //访问token数据库  
    private final PasswordEncoder passwordEncoder; //密码加密器  
    private final JwtService jwtService; //JWT 相关方法  
    private final AuthenticationManager authenticationManager; //Spring Security 认证管理器  

    /**  
    * 注册方法  
    * @param request 请求体  
    * @return AuthenticationResponse(自己封装的响应结构)  
    */  
    public AuthenticationResponse register(RegisterRequest request) {
    
      
    //构建用户信息  
        var user = User.builder()  
        .firstname(request.getFirstname())  
        .lastname(request.getLastname())  
        .email(request.getEmail())  
        .password(passwordEncoder.encode(request.getPassword()))  
        .role(request.getRole())  
        .build();  

        //将用户信息保存到数据库  
        var savedUser = repository.save(user);  
        //通过JWT方法生成Token  
        var jwtToken = jwtService.generateToken(user);  
        //生成RefreshToken(刷新Token使用)  
        var refreshToken = jwtService.generateRefreshToken(user);  
        //将Token保存到数据库  
        saveUserToken(savedUser, jwtToken);  
        //返回响应体  
        return AuthenticationResponse.builder()  
        .accessToken(jwtToken)  
        .refreshToken(refreshToken)  
        .build();  
    }  

    /**  
    * 鉴权(登录)方法  
    * @param request 请求体  
    * @return AuthenticationResponse(自己封装的响应结构)  
    */  
    public AuthenticationResponse authenticate(AuthenticationRequest request) {
    
      
        //通过Spring Security 认证管理器进行认证  
        //如果认证失败会抛出异常 eg:BadCredentialsException 密码错误 UsernameNotFoundException 用户不存在  
        authenticationManager.authenticate(  
        new UsernamePasswordAuthenticationToken(  
        request.getEmail(),  
        request.getPassword()  
        )  
        );  
        //通过邮箱查询用户信息,当前项目email就是账号  
        var user = repository.findByEmail(request.getEmail())  
        .orElseThrow();  
        //通过JWT方法生成Token  
        var jwtToken = jwtService.generateToken(user);  
        //生成RefreshToken(刷新Token使用)  
        var refreshToken = jwtService.generateRefreshToken(user);  
        //将之前所有的Token变成失效状态  
        revokeAllUserTokens(user);  
        //保存新的Token到数据库  
        saveUserToken(user, jwtToken);  
        //封装响应体  
        return AuthenticationResponse.builder()  
        .accessToken(jwtToken)  
        .refreshToken(refreshToken)  
        .build();  
    }  

    /**  
    * 保存用户Token方法  
    * 构建Token实体后保存到数据库  
    * @param user 用户信息  
    * @param jwtToken Token  
    */  
    private void saveUserToken(User user, String jwtToken) {
    
      
        var token = Token.builder()  
        .user(user)  
        .token(jwtToken)  
        .tokenType(TokenType.BEARER)  
        .expired(false)  
        .revoked(false)  
        .build();  
        tokenRepository.save(token);  
    }  

    /**  
    * 将用户所有Token变成失效状态  
    * @param user 用户信息  
    */  
    private void revokeAllUserTokens(User user) {
    
      
        //获取用户所有有效的token  
        var validUserTokens = tokenRepository.findAllValidTokenByUser(user.getId());  
        if (validUserTokens.isEmpty()){
    
      
        return;  
        }  
        //如果存在还为失效的token,将token置为失效  
        validUserTokens.forEach(token -> {
    
      
        token.setExpired(true);  
        token.setRevoked(true);  
        });  
        tokenRepository.saveAll(validUserTokens);  
    }  

    /**  
    * 刷新token方法  
    * @param request 请求体  
    * @param response 响应体  
    * @throws IOException 抛出IO异常  
    */  
    public void refreshToken(  
    HttpServletRequest request,  
    HttpServletResponse response  
    ) throws IOException {
    
      
        //从请求头中获取中获取鉴权信息 AUTHORIZATION  
        final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);  
        final String refreshToken;  
        final String userEmail;  
        //如果鉴权信息为空或者不是以Bearer 开头的,直接返回  
        if (authHeader == null ||!authHeader.startsWith("Bearer ")) {
    
      
            return;  
        }  
        //从鉴权信息中获取RefreshToken  
        refreshToken = authHeader.substring(7);  
        //从RefreshToken中获取用户信息  
        userEmail = jwtService.extractUsername(refreshToken);  
        if (userEmail != null) {
    
      
            //根据用户信息查询用户,如果用户不存在抛出异常  
            var user = this.repository.findByEmail(userEmail)  
            .orElseThrow();  

            //验证Token是否有效  
            if (jwtService.isTokenValid(refreshToken, user)) {
    
      
                //生成新的Token  
                var accessToken = jwtService.generateToken(user);  
                revokeAllUserTokens(user);  
                saveUserToken(user, accessToken);  
                //生成新的Token和RefreshToken并通过响应体返回  
                var authResponse = AuthenticationResponse.builder()  
                .accessToken(accessToken)  
                .refreshToken(refreshToken)  
                .build();  
                new ObjectMapper().writeValue(response.getOutputStream(), authResponse);  
            }  
        }  
    }  
}

The above code mainly explains the process of returning after 注册& . In the current project, due to the long validity period of & , we chose to save it to the database (personal opinion!!!). You can decide whether you need to save it according to the actual needs of your business.登录tokentokenrefreshTokentokenredis

3.5 Request filtering

Request filtering is mainly used to dynamically parse and tokenobtain the information during each request to ensure the security of the requested resources and prevent unauthorized access, etc.用户信息权限

@Component  
@RequiredArgsConstructor  
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
      
  
    private final JwtService jwtService;  
    private final UserDetailsService userDetailsService;  
    private final TokenRepository tokenRepository;  

    @Override  
    protected void doFilterInternal(  
    @NonNull HttpServletRequest request,  
    @NonNull HttpServletResponse response,  
    @NonNull FilterChain filterChain  
    ) throws ServletException, IOException {
    
      
        //判断请求是否为登录请求,如果是登录请求则不进行处理  
        if (request.getServletPath().contains("/api/v1/auth")) {
    
      
            filterChain.doFilter(request, response);  
            return;  
        }  
        //从请求头中获取鉴权authHeader  
        final String authHeader = request.getHeader("Authorization");  
        final String jwt;  
        final String userEmail;  

        //如果不存在Token或者Token不已Bearer开头,则不进行处理  
        if (authHeader == null ||!authHeader.startsWith("Bearer ")) {
    
      
            filterChain.doFilter(request, response);  
            return;  
        }  
        //从authHeader中截取出Token信息  
        jwt = authHeader.substring(7);  
        //从Token中获取userEmail(账户)  
        userEmail = jwtService.extractUsername(jwt);  
        //SecurityContextHolder 中的 Authentication 为空时,才进行处理  
        if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
    
      
            //获取用户信息  
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);  

            //从数据库中查询Token并判断Token状态是否正常  
            var isTokenValid = tokenRepository.findByToken(jwt)  
                .map(t -> !t.isExpired() && !t.isRevoked())  
                .orElse(false);  

            //如果Token有效,并且Token状态正常,将用户信息存储到SecurityContextHolder  
            if (jwtService.isTokenValid(jwt, userDetails) && isTokenValid) {
    
      
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(  
                userDetails, //用户信息  
                null,  
                userDetails.getAuthorities() //用户的权限  
                );  
                authToken.setDetails(  
                    new WebAuthenticationDetailsSource().buildDetails(request) //访问信息  
                );  
                //将用户信息以及权限保存到 SecurityContextHolder的上下文中,方便后续使用  
                //eg: 获取当前用户id,获取当前用户权限等等  
                SecurityContextHolder.getContext().setAuthentication(authToken);  
            }  
        }  
        filterChain.doFilter(request, response);  
    }  
}

The main logic of the above code is: obtain the validity of tokenthe verification from the request header tokenand tokenstore the parsed information SecurityContextHolderin the context to facilitate subsequent use.

3.6 Log out

登录And tokenthe verification has been mentioned, now we are just one step away from logging out. Do you still remember that we configured a 退出登录request path before: /api/v1/auth/logoutWhen we request this path, securityit will help us find the corresponding one LogoutHandler, and then call logoutthe method Implement logout.

@Service  
@RequiredArgsConstructor  
public class LogoutService implements LogoutHandler {
    
      

    private final TokenRepository tokenRepository;  

    @Override  
    public void logout(  
    HttpServletRequest request,  
    HttpServletResponse response,  
    Authentication authentication  
    ) {
    
      
        //从请求头中获取鉴权信息  
        final String authHeader = request.getHeader("Authorization");  
        final String jwt;  
        if (authHeader == null ||!authHeader.startsWith("Bearer ")) {
    
      
        return;  
        }  
        //接续出token  
        jwt = authHeader.substring(7);  
        //从数据库中查询出token信息  
        var storedToken = tokenRepository.findByToken(jwt)  
        .orElse(null);  
        if (storedToken != null) {
    
      
            //设置token过期  
            storedToken.setExpired(true);  
            storedToken.setRevoked(true);  
            tokenRepository.save(storedToken);  
            //清除SecurityContextHolder上下文  
            SecurityContextHolder.clearContext();  
        }  
    }  
}

securityIt has done a lot of things for us. We only need to tokenset it to the invalid state and then clear SecurityContextHolderthe context to solve all the problems.

4 Authentication

The following uses several examples to explain two different authentication configuration methods.

4.1 controller

@RestController  
@RequestMapping("/api/v1/admin")  
@PreAuthorize("hasRole('ADMIN')") //用户需要ADMIN角色才能访问  
public class AdminController {
    
      
  
    @GetMapping  
    @PreAuthorize("hasAuthority('admin:read')") //用户需要admin:read权限才能访问  
    public String get() {
    
      
        return "GET:: admin controller";  
    }  
    @PostMapping  
    @PreAuthorize("hasAuthority('admin:create')") //用户需要admin:create权限才能访问  
    @Hidden  
    public String post() {
    
      
        return "POST:: admin controller";  
    }  
    @PutMapping  
    @PreAuthorize("hasAuthority('admin:update')")  
    @Hidden  
    public String put() {
    
      
        return "PUT:: admin controller";  
    }  
    @DeleteMapping  
    @PreAuthorize("hasAuthority('admin:delete')")  
    @Hidden  
    public String delete() {
    
      
        return "DELETE:: admin controller";  
    }  
}

4.2 Configuration file

SecurityConfigurationPart of the code for the configuration class is posted below

Insert image description here

Guess you like

Origin blog.csdn.net/weixin_55756734/article/details/133082139