SpringSecurity使用JWT

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

新建SpringBoot项目,添加依赖:


org.springframework.boot
spring-boot-starter-security


org.springframework.boot
spring-boot-starter-web


io.jsonwebtoken
jjwt
0.9.0


org.projectlombok
lombok


javax.xml.bind
jaxb-api
2.3.1

我使用的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  阅读(202)  评论(0)    收藏  举报