SpringSecurity基于表单的登录认证流程如下图:
大致过程是在UsernamePasswordAuthenticationFilter将username和password封装成UsernamePasswordAuthenticationToken,之后会在AuthenticationProvider(接口,需要自己实现)的authenticate方法中拿到且校验(一般是查数据库,对比账号密码),而校验成功则返回Authentication接口对象(一般情况是直接返回UsernamePasswordAuthenticationToken,该对象会默认记录在session以及SecurityContextHolder的SecurityContext中)随后走认证成功流程(一般会实现AuthenticationSuccessHandler接口),否则就抛异常走认证失败流程(一般会实现AuthenticationFailureHandler接口)。
以上是普通单体项目的认证过程,那么整合JWT或者说微服务则会有以下问题:
- ?在哪里生成JWT?
- 如何解释前端传来的JWT字符串获取到用户信息?
- 认证成功后如何去除Session信息?(JWT是无状态的,不需要服务器端保存)JWT的详情参考
对于第一个问题,很好解决,在AuthenticationProvider认证成功后不返回UsernamePasswordAuthenticationToken,返回自定义的JwtAuthenticationToken,然后在自定义的LoginSuccessHandler返回给前端即可,代码如下:
//JWT中荷载存储内容的实体类
@Data
public class UserDetails {
private String userId;
private String username;
private String portrait;
}
//用来生成和解释Token字符串,这里采用了单例模式,相当于JwtUtils
public class UserTokenManager {
private final String SECRET = "happy-king";
private static volatile UserTokenManager userTokenManager;
private UserTokenManager() {
}
public static UserTokenManager getInstance() {
if (userTokenManager == null) {
synchronized (UserTokenManager.class) {
if (userTokenManager == null) {
userTokenManager = new UserTokenManager();
}
}
}
return userTokenManager;
}
@SuppressWarnings("unchecked")
public String generateToken(UserDetails userDetails, long timeout) {
long currentTimeMillis = System.currentTimeMillis();
return JWT.create()
.withJWTId(UUID.randomUUID().toString())
.withIssuedAt(new Date(currentTimeMillis))
.withClaim("user_id", userDetails.getUserId())
.withClaim("username", userDetails.getUsername())
.withClaim("portrait", userDetails.getPortrait())
.withExpiresAt(new Date(currentTimeMillis + timeout))
.sign(Algorithm.HMAC256(SECRET));
}
public String generateToken(UserDetails userDetails) {
return generateToken(userDetails, 1000 * 24 * 60 * 60L);
}
public UserDetails parseToken(String token) {
try {
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
Date issuedAt = decodedJWT.getIssuedAt();
if (issuedAt.before(new Date())) {
String userId = decodedJWT.getClaim("user_id").asString();
String username = decodedJWT.getClaim("username").asString();
String portrait = decodedJWT.getClaim("portrait").asString();
UserDetails userDetails = new UserDetails();
userDetails.setUsername(username);
userDetails.setUserId(userId);
userDetails.setPortrait(portrait);
return userDetails;
} else {
return null;
}
} catch (Exception e) {
return null;
}
}
}
//自定义的Authentication类
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private final UserDetails userDetails;
private final UserTokenManager userTokenManager = UserTokenManager.getInstance();
public JwtAuthenticationToken(UserDetails userDetails,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.userDetails = userDetails;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return userDetails;
}
public String generateJWT(){
return userTokenManager.generateToken(userDetails);
}
}
//登录认证主要逻辑类
public class AuthenticationProvider implements org.springframework.security.authentication.AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
//获取账号密码
String username = (String) token.getPrincipal();
String password = (String) token.getCredentials();
//省略了查数据库以及对比流程
UserDetails userDetails = new UserDetails();
userDetails.setUsername(username);
userDetails.setUserId(UUID.randomUUID().toString());
//在微服务中,角色和权限信息要保存在Redis中
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_xx");
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(userDetails, Collections.singleton(simpleGrantedAuthority));
//必须设置为认证成功
jwtAuthenticationToken.setAuthenticated(true);
return jwtAuthenticationToken;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication == JwtAuthenticationToken.class || authentication == UsernamePasswordAuthenticationToken.class;
}
}
//然后登录成功直接写token响应到前端即可
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
private final ObjectMapper objectMapper;
public LoginSuccessHandler(ObjectMapper objectMapper){
this.objectMapper = objectMapper;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
response.setContentType("application/json;charset=utf-8");
HashMap<String, Object> map = new HashMap<>();
map.put("token", jwtAuthenticationToken.generateJWT());
String result = objectMapper.writeValueAsString(map);
response.getWriter().write(result);
}
}
需要注意必须执行以下代码:
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(userDetails, Collections.singleton(simpleGrantedAuthority));
jwtAuthenticationToken.setAuthenticated(true);
因为除了认证接口外,其他所有需要授权()的接口都会被AbstractSecurityInterceptor拦截,里面有一个authenticateIfRequired方法,如果没有调用
jwtAuthenticationToken.setAuthenticated(true);则会重新进行用户认证。源码如下:
private Authentication authenticateIfRequired() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.isAuthenticated() && !this.alwaysReauthenticate) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Did not re-authenticate %s before authorizing", authentication));
}
return authentication;
}
authentication = this.authenticationManager.authenticate(authentication);
// Don't authenticated.setAuthentication(true) because each provider does that
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Re-authenticated %s before authorizing", authentication));
}
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
}
第二个问题则需要用到SpringSecurity中的拦截器,我们需要自定义一个JwtFilter,并且将其加入到UsernamePasswordAuthenticationFilter之后,代码如下:
public final class JwtFilter extends GenericFilterBean {
private final static String BEARER = "bearer";
private final UserTokenManager userTokenManager = UserTokenManager.getInstance();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
/**
*以下抛出的异常你可以改为写出JSON响应
**/
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
chain.doFilter(request, response);
return;
}
String bearer = request.getHeader(BEARER);
if (bearer == null || bearer.isEmpty()){
bearer = request.getParameter(BEARER);
}
if (bearer == null || bearer.isEmpty()){
//没有登录
throw new AuthenticationCredentialsNotFoundException("没有用户凭证");
}else{
UserDetails userDetails = userTokenManager.parseToken(bearer);
if (userDetails == null){
throw new CredentialsExpiredException("用户凭证失效");
}
//微服务项目角色权限信息需要重Redis中获取,这里只是模拟
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_xx");
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(userDetails, Collections.singleton(simpleGrantedAuthority));
jwtAuthenticationToken.setAuthenticated(true);
//注意要将解释出来的JwtAuthenticationToken信息保存到SecurityContext中
SecurityContextHolder
.getContext()
.setAuthentication(jwtAuthenticationToken);
}
chain.doFilter(request, response);
}
}
至此单体项目整合SpringSecurit 和JWT登录已经完成,用户登录成功后,SecurityContextPersistenceFilter中的SecurityContextRepository(默认实现是HttpSessionSecurityContextRepository)会将JwtAuthenticationToken写到session中。但是因为JWT用于分布式系统或者微服务的,所以我们不能用Session来管理,只需要修改默认的HttpSessionSecurityContextRepository为NullSecurityContextRepository并关闭session管理即可。
代码如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
//移除默认登录页面
http.removeConfigurer(DefaultLoginPageConfigurer.class);
//关闭csrf
http.csrf().disable()
//关闭请求缓存
.requestCache().disable()
//关闭session管理
.sessionManagement().disable()
//修改SecurityContextRepository
.securityContext().securityContextRepository(new NullSecurityContextRepository()).and()
.addFilterAfter(new JwtFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests().anyRequest().permitAll().and()
.formLogin()
.loginPage("/login")
.failureHandler(new LoginFailureHandler(objectMapper))
.successHandler(new LoginSuccessHandler(objectMapper))
.permitAll()
.and()
.httpBasic();
}
?至此,整合工作已经完毕,下面提供了代码下载地址,以上流程和WebSecurityConfigurerAdapter的实现配置用于用户微服务,然后在不同的服务下定义不同的WebSecurityConfigurerAdapter然后设置formLogin().disable()即可
示例代码下载
?
?
?
?
|