JWT
是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT
作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以json
对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT
可以使用HMAC
算法或者是RSA
的公私秘钥对进行签名。
JWT
主要包含三个部分之间用英语句号'.'隔开
- Header 头部
- Payload 负载
- Signature 签名
注意,顺序是 header.payload.signature
目前系统开发经常使用前后端分离的模式,前端使用vue
等框架,调用后端的rest接口返回json
格式的数据,并在前端做展示。登录成功后,后台会向前端返回一个token,前端每次访问后台接口时都携带令牌(在header中携带令牌信息),后台对令牌信息进行校验,如果校验成功可访问后台接口。
(一)实现思路
- 我们使用
auth0
的java-jwt
是一个JSON WEB TOKEN(JWT)
的一个实现; - 登录认证成功后,根据一定的规则生成token,并把token返回给前端;
- 增加一个过滤器,对每次请求进行过滤(除登录请求外),查看请求头是否携带有token信息,如果携带有token信息,则对token进行验证,验证通过则进行下一步,验证不通过则返回相应异常信息;前端根据异常信息做出操作。
(二)具体步骤
1、引入依赖
引入com.auth0
的依赖,用来生成token信息
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
2、生成token
在UserDetailsService
的实现类里增加生成token的方法
/**
* 保存用户信息
* @param userDetails
*/
public User saveUserLoginInfo(UserDetails userDetails) {
/**
获取用户信息,此处可修改为从redis中获取用户信息
*/
User user = userMapper.getUserByName(userDetails.getUsername());
if (user != null) {
String salt = user.getSalt();
Date loginTime = user.getLastLogin();
// 挑出部分用户信息,生成token
User tokenUser = new User();
tokenUser.setId(user.getId());
tokenUser.setName(user.getName());
// 如果需要重复登陆保持 token
boolean useOldToken = false;
// 需要重复登陆保持 token,但没有登陆过,或上次登陆已过期,生成新的 salt 与 loginTime,生成新的 token
if (!useOldToken) {
salt = BCrypt.gensalt();
loginTime = new Date();
user.setSalt(salt);
user.setLastLogin(loginTime);
userMapper.updateSalt(user.getId(), salt);
}
// 生成token
Algorithm algorithm = Algorithm.HMAC256(salt);
Date expiresTime = expiresTime(loginTime);
//使用jwt的API生成token
String token = JWT.create()
//面向用户的值
.withSubject(JsonUtil.toJson(tokenUser)).
//过期时间
withExpiresAt(expiresTime)
//签发时间
.withIssuedAt(loginTime)
//签名算法
.sign(algorithm);
log.info("JWT Token is generated at {} for user {}, and will be expired at {}", df.format(loginTime), user.getName(), df.format(expiresTime));
// 添加或更新缓存 可在此处把用户信息更新或添加到缓存中
user.setToken(token);
return user;
}
return null;
}
private Date expiresTime(Date time) {
Calendar expiresTime = Calendar.getInstance();
expiresTime.setTime(time);
expiresTime.add(Calendar.SECOND, 3600);
return expiresTime.getTime();
}
3、增加过滤器、token校验
新开发一个过滤器,对请求进行拦截并验证token,如果token没问题,则放行,如果token异常则返回异常信息给前端。
新增加token校验的服务,对token进行解析及验证是否有效、是否过期。
public class JWTFilter extends OncePerRequestFilter {
private RequestMatcher requiresAuthenticationRequestMatcher;
private List<RequestMatcher> permissiveRequestMatchers;
private TokenService tokenService;
private SecurityUserDetailsService userMapper;
private AuthenticationSuccessHandler successHandler ;
private AuthenticationFailureHandler failureHandler = new AuthFailureHandler();
public JWTFilter(TokenService tokenService, SecurityUserDetailsService userMapper) {
this.requiresAuthenticationRequestMatcher =
new RequestHeaderRequestMatcher("Authorization");
this.tokenService=tokenService;
this.userMapper=userMapper;
}
@Override
public void afterPropertiesSet() {
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (!requiresAuthentication(request, response)) {
filterChain.doFilter(request, response);
return;
}
String uri = request.getRequestURI();
// 不拦截登陆
if ( "/login".equals(uri)) {
filterChain.doFilter(request, response);
return;
}
// 从请求头中获取token
String token = request.getHeader("Authorization");
//把token解析为jwt对象
DecodedJWT jwt = tokenService.decode(token);
//从数据库或缓存中获取user对象
User user =tokenService.retrieve(jwt);
// 退出时只检验 token 的合法性,是否能解析出来user对象
if ("/logout".equals(uri)) {
try {
tokenService.analytic(token);
// 上下文中缓存用户
} catch (Exception e) {
unsuccessfulAuthentication(request, response, new InternalAuthenticationServiceException("", e));
}
filterChain.doFilter(request, response);
return;
}
//查询用户权限生成Authentication对象,这里直接写静态代码,项目中需要从db中查找用户相应的角色
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
authorities.add(new SimpleGrantedAuthority("ROLE_ROOT"));
Authentication authResult = new UsernamePasswordAuthenticationToken(user.getName(),user.getPassword(),authorities);
AuthenticationException failed = null;
try {
tokenService.validate(token);
} catch (Exception e) {
failed = new InternalAuthenticationServiceException("", e);
}
if (failed == null) {
successfulAuthentication(request, response, filterChain, authResult,token);
} else if (!permissiveRequest(request)) {
unsuccessfulAuthentication(request, response, failed);
return;
}
filterChain.doFilter(request, response);
}
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
// token 校验失败
failureHandler.onAuthenticationFailure(request, response, failed);
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult,String token) throws IOException, ServletException {
/**
*验证成功可以根据业务需求做一系列操作后,请求继续往下进
* 比如把用户信息放入threadlocal中,供后续操作使用1
*/
SecurityContextHolder.getContext().setAuthentication(authResult);
//根据用户名查询user对象
//获取token
DecodedJWT jwt = tokenService.decode(token);
//判断是否应该刷新token
if(shouldTokenRefresh(jwt.getExpiresAt())){
User user =userMapper.saveUserLoginInfo((UserDetails) authResult.getPrincipal());
String newToken =user.getToken();
response.setHeader("Authorization", newToken);
}
}
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return requiresAuthenticationRequestMatcher.matches(request);
}
protected boolean permissiveRequest(HttpServletRequest request) {
if (permissiveRequestMatchers == null) {
return false;
}
for (RequestMatcher permissiveMatcher : permissiveRequestMatchers) {
if (permissiveMatcher.matches(request)) {
return true;
}
}
return false;
}
/**
* 判断是否应该刷新token
* @param expireAt
* @return
*/
protected boolean shouldTokenRefresh(Date expireAt) {
LocalDateTime expireTime = LocalDateTime.ofInstant(expireAt.toInstant(), ZoneId.systemDefault());
LocalDateTime freshTime = expireTime.minusSeconds(1200);
// log.info("Check token refresh, token will expire at {}, need refresh after {}", expireTime.format(dtf), freshTime.format(dtf));
return LocalDateTime.now().isAfter(freshTime);
}
@Component
public class TokenService {
@Autowired
private UserMapper userMapper;
private Logger log = LoggerFactory.getLogger(TokenService.class);
/**
* 只单纯解码 token,取出其中的用户信息
*
* @param token
* @return
*/
public DecodedJWT decode(String token) {
if (token == null) {
throw new RuntimeException("用户未验证");
}
DecodedJWT jwt = null;
try {
jwt = JWT.decode(token);
} catch (JWTDecodeException e1) {
log.warn("Jwt decode token failed, msg is: {}", e1.getLocalizedMessage());
throw new RuntimeException("token解析错误");
}
return jwt;
}
/**
* 从 jwt 中解析出用户信息
*
* @param token
* @return
*/
public User analytic(String token) {
return analytic(decode(token));
}
/**
* 从 jwt 中解析出用户信息
*
* @param jwt
* @return
*/
public User analytic(DecodedJWT jwt) {
User user = null;
try {
user = JsonUtil.toObject(jwt.getSubject(), User.class);
} catch (Exception e) {
log.warn("Jwt subject convert to User failed, msg is: {}", e.getLocalizedMessage());
throw new RuntimeException("用户未认证");
}
return user;
}
/**
* 解码 token,并从缓存中或者数据库中取回用户的详细信息
* @param jwt
* @return
*/
public User retrieve(DecodedJWT jwt) {
User user = null;
try {
user = userMapper.getUserByName(analytic(jwt).getName());
} catch (Exception e) {
log.warn("Retrieve user from redis cache failed, msg is: {}", e.getLocalizedMessage());
}
if (user == null) {
throw new RuntimeException("用户未登录");
}
return user;
}
/**
* 校验 token 是否合法
* @param token
* @return
*/
public void validate(String token) {
validate(decode(token), null);
}
/**
* 校验 token 是否合法
*
* @param jwt
* @param cofUser 从缓存中可以取得
* @return
*/
public void validate(DecodedJWT jwt, User user) {
// 是否超时
if (Calendar.getInstance().getTime().after(jwt.getExpiresAt())) {
throw new RuntimeException("token验证失败");
}
// 取用户
if (user == null) {
user = retrieve(jwt);
}
if (user == null) {
throw new RuntimeException("token验证失败");
}
// 用户中不含 salt
if (user.getSalt() == null) {
throw new RuntimeException("token验证失败");
}
// 校验用户状态, 只有为 enabled 的用户才允许登陆
if ("ENABLED".equals(user.getStatus())) {
throw new RuntimeException("token验证失败");
}
// 校验token是否合法
String encryptSalt = user.getSalt();
try {
Algorithm algorithm = Algorithm.HMAC256(encryptSalt);
JWTVerifier verifier = JWT.require(algorithm).withSubject(jwt.getSubject()).build();
verifier.verify(jwt.getToken());
} catch (Exception e) {
log.warn("Jwt verifier token failed, msg is: {}", e.getLocalizedMessage());
throw new RuntimeException("token验证失败");
}
}
}
4、修改配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(imageCodeFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(getJwtFilter(),ImageCodeFilter.class) //在imageCodeFilter后面加JwtFilter
.authorizeRequests()
.antMatchers("/imageCode").permitAll()
.antMatchers("/hello/admin").hasRole("ROOT")
.antMatchers("/hello").hasRole("USER").anyRequest().permitAll()
.and()
.csrf().disable().
formLogin().loginPage("/login") //自定义登录页面跳转
.defaultSuccessUrl("/hello")
.successForwardUrl("/hello/admin")//登录成功后跳转
.successHandler(authSuccessHandler)
.failureHandler(authFailureHandler)
.and().httpBasic().disable()
.sessionManagement().disable()
.cors()
.and()
.logout().logoutUrl("/logout").addLogoutHandler(authLogoutHandler);
}
/**
* 加密方式 配置对token的验证过滤器
* @return
*/
@Bean
protected JWTFilter getJwtFilter(){
return new JWTFilter(tokenService,securityUserDetailsService);
}