SpringSecurity使用JWT

SpringSecurity的UsernamePasswordAuthenticationFilter用于处理认证。要整合JWT,只需在认证成功后生成TOKEN并通过响应头写回客户端。在新增一个过滤器用于校验TOKEN。

新建SpringBoot项目,添加依赖:

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

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

     <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.3.1</version>
    </dependency>

我使用的JAVA版本是17,版本较高,需要添加jaxb-api依赖。

 

在application.properties配置

#60分钟
token.expire=3600000
token.key=123456HJKsdsf,';dfs

配置TOKEN的有效时间和秘钥。

 

增加TOKEN管理接口:

public interface TokenManager {
    public String createToken(String username);

    public String getUserFromToken(String token);
}

增加实现类:

@Service
@Slf4j
public class JwtTokenManager implements TokenManager {

    @Value("${token.expire}")
    private long tokenExpiration = 3600;
    @Value("${token.key}")
    private String tokenSignKey;

    @Override
    public String createToken(String username) {
        String token = Jwts.builder().setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
                .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
        log.info("用户:{}生成token:{}", username, token);
        return token;
    }


    @Override
    public String getUserFromToken(String token) {
        String user = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
        log.info("从token:{}解析的用户名:{}", token, user);
        return user;
    }
}

 

增加常量:

public  final class GlobalConstant {

    private GlobalConstant() {

    }
    public final static String TOKEN_NAME = "MY_TOKEN";

}

保存TOKEN的名字。

 

配置SpringSecurity:

@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private TokenManager tokenManager;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password("123")
                .roles("USER")
                .and()
                .withUser("user")
                .password("123")
                .roles("TEMP");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .successHandler((request,response,authentication) -> {
                    UserDetails  userDetails = (UserDetails) authentication.getPrincipal();
                    String token = tokenManager.createToken(userDetails.getUsername());
                    response.setHeader(TOKEN_NAME, token);

                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    out.write("登录成功");
                    out.flush();
                    out.close();
                })
                .failureHandler(((request, response, exception) -> {
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    out.write(exception.getMessage());
                    out.flush();
                    out.close();
                }))
                .permitAll()
                .and()
                .logout()
                .logoutSuccessHandler(((request, response, authentication) -> {
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    out.write("注销成功");
                    out.flush();
                    out.close();
                }))
                .permitAll()
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(((request, response, authException) -> {
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    out.write("尚未登录,请先登录");
                    out.flush();
                    out.close();
                }))
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .csrf()
                .disable()
                ;


        JwtAuthencationFilter jwtAuthencationFilter = new JwtAuthencationFilter(tokenManager, http.getSharedObject(UserDetailsService.class));
        http.addFilterBefore(jwtAuthencationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

为了简单,将用户信息保存到内存中。successHandler用于处理认证成功后的操作,这里生成TOKEN并写到客户端。failureHandler用于处理认证失败的操作。配置后默认登录页不生效。logoutSuccessHandler用于处理注销成功后的操作。authenticationEntryPoint用于处理未登录的操作。JwtAuthencationFilter用于校验TOKEN。JwtAuthencationFilter加到UsernamePasswordAuthenticationFilter的前面。还配置了session的创建策略为SessionCreationPolicy.STATELESS,即不创建Session。

 

public class JwtAuthencationFilter extends OncePerRequestFilter {

    private TokenManager tokenManager;

    private UserDetailsService userDetailsService;

    public JwtAuthencationFilter(TokenManager tokenManager, UserDetailsService userDetailsService) {
        this.tokenManager = tokenManager;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader(TOKEN_NAME);

        if (token != null) {
            String username = tokenManager.getUserFromToken(token);

            if (username != null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (userDetails == null) {
                    throw new RuntimeException("非法TOKEN");
                }

                SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(username, token, new ArrayList<>()));
            } else {
                throw new RuntimeException("非法TOKEN");
            }
        }

        filterChain.doFilter(request, response);
    }
}

JwtAuthencationFilter从TOKEN中取出用户名,校验用户是否有效。这里还可以将用户保存到redis,避免每次都要查询数据库。还可以校验TOKEN的有效性,以及TOKEN的续期。如果访问不带TOKEN,则由配置的authenticationEntryPoint处理未登录情形。

 

用Postman以POST方式调用登录接口,UsernamePasswordAuthenticationFilter默认是以表单登录的,如果要以JSON格式参数登录,可以继承UsernamePasswordAuthenticationFilter增加解析JSON参数。获取到TOKEN后在访问controller,发现访问成功。去掉TOKEN后,再访问controller发现访问失败。

posted @ 2023-06-04 14:10  shigp1  阅读(164)  评论(0)    收藏  举报