学生程序设计能力提升平台 Spring Security的应用(三)
JSON WEB TOKEN与spring security
json web token简介
JWT 是一个很长的字符串,由 .
分割为三段
Header(头部)
存储 JWT 的元数据,与生成 Token API jwt.sign(payload, secretOrPrivateKey, [options, callback])
中的 options
对应
Payload(负载数据)
存放需要实际传递的数据,官方定义了7个官方字段
Signature(签名)
是对前两部分的签名,防止数据篡改
JwtUtil工具类
/**
* JWT工具类
* @author widealpha
* @date 2021/7/13
*/
public class JwtUtil {
private static final String SECRET = "secret"; //JWT签证密钥
private static final String ROLE = "ROLE"; //Jwt中携带的身份key
private static final String USER_ID = "USER_ID"; //Jwt中携带的用户ID的key
private static final Long EXPIRATION = 60 * 60 * 24 * 7L; //过期时间7天
public static final String TOKEN_HEADER = "Authorization"; //Header标识JWT
public static final String TOKEN_PREFIX = "Bearer "; //JWT标准开头,注意空格
/**
* 创建JWT
* @param username 账户名
* @param userId 用户ID
* @param roles 用户角色,以英文逗号(,)分隔开
* @return 创建好的Token
*/
public static String createToken(String username,Integer userId, String roles) {
Map<String, Object> map = new HashMap<>();
map.put(ROLE, roles);
map.put(USER_ID, userId);
return Jwts.builder().signWith(SignatureAlgorithm.HS256, SECRET)
.setClaims(map).setSubject(username).setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
.compact();
}
/**
* 根据token获取用户名
* @param token JWT
* @return 用户名
*/
public static String getUsername(String token) {
try {
return getTokenBody(token).getSubject();
} catch (Exception e) {
return null;
}
}
/**
* 获取Token载体信息
* @param token JWT
* @return token携带的claim
*/
private static Claims getTokenBody(String token) {
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
e.printStackTrace();
}
return claims;
}
/**
* 获取token携带的用户角色列表
* @param token JWT
* @return 用户角色,以英文逗号(,)分隔开
*/
public static String getUserRole(String token) {
return (String) getTokenBody(token).get(ROLE);
}
/**
* 获取token携带的用户ID
* @param token JWT
* @return 用户ID
*/
public static Integer getUserId(String token){
return (Integer) getTokenBody(token).get(USER_ID);
}
/**
* 判断token是否国企
* @param token JWT
* @return 是否过期
*/
public static Boolean isExpiration(String token) {
try {
return getTokenBody(token).getExpiration().before(new Date());
} catch (Exception e) {
System.out.println(e.getMessage());
}
return true;
}
/**
* 获取签发日期
* @param token JWT
* @return 签发Token的日期
*/
public static Date getIssuedAt(String token){
return getTokenBody(token).getIssuedAt();
}
}
通过工具类可以十分便捷的获取到所有的token的属性等数据
Config
依然首先是通过config进行注入配置
@Configuration
public class JwtLoginConfig<T extends JwtLoginConfig<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T, B> {
private final JwtAuthenticationFilter authFilter;
public JwtLoginConfig() {
this.authFilter = new JwtAuthenticationFilter();
}
@Override
public void configure(B http) {
authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
authFilter.setAuthenticationFailureHandler(new HttpStatusLoginFailureHandler());
//将filter放到logoutFilter之前
JwtAuthenticationFilter filter = postProcess(authFilter);
http.addFilterBefore(filter, LogoutFilter.class);
}
//设置匿名用户可访问url
public JwtLoginConfig<T, B> permissiveRequestUrls(String... urls) {
authFilter.setPermissiveUrl(urls);
return this;
}
public JwtLoginConfig<T, B> tokenValidSuccessHandler(AuthenticationSuccessHandler successHandler) {
authFilter.setAuthenticationSuccessHandler(successHandler);
return this;
}
}
在这里代码里把filter放置在Logout之前,是因为logout是需要鉴权的操作,每次logout都必须有授权才能logout
这里通过配置filter实现登录请求的拦截
对一些数据的实体类进行IOC控制反转,降低内存的使用
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final RequestHeaderRequestMatcher requiresAuthenticationRequestMatcher;
private List<RequestMatcher> permissiveRequestMatchers;
private AuthenticationManager authenticationManager;
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
public JwtAuthenticationFilter() {
//拦截header中带Authorization的请求
this.requiresAuthenticationRequestMatcher = new RequestHeaderRequestMatcher("Authorization");
}
protected String getJwtToken(HttpServletRequest request) {
return request.getHeader(JwtUtil.TOKEN_HEADER);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!requiresAuthentication(request)) {
chain.doFilter(request, response);
return;
}
String tokenHeader = getJwtToken(request);
if (tokenHeader == null || !tokenHeader.startsWith(JwtUtil.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
try {
if (!JwtUtil.isExpiration(tokenHeader.replace(JwtUtil.TOKEN_PREFIX, ""))) {
UsernamePasswordAuthenticationToken authentication = getAuthentication(tokenHeader);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (ExpiredJwtException e) {
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "text/html;charset=UTF-8");
response.getWriter().print(ResultEntity.error(StatusCode.USER_TOKEN_OVERDUE));
return;
} catch (JwtException e) {
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "text/html;charset=UTF-8");
response.getWriter().print(ResultEntity.error(StatusCode.USER_TOKEN_ERROR));
return;
} catch (Exception e) {
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "text/html;charset=UTF-8");
response.getWriter().print(ResultEntity.error(StatusCode.COMMON_FAIL));
return;
}
chain.doFilter(request, response);
}
//获取用户信息
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
String token = tokenHeader.replace(JwtUtil.TOKEN_PREFIX, "");
String account = JwtUtil.getUsername(token);
Integer userId = JwtUtil.getUserId(token);
if (account == null || userId == null) {
return null;
}
// 获得权限 添加到权限上去
String roles = JwtUtil.getUserRole(token);
PtaUser ptaUser = new PtaUser(account, "[PROTECTED]", AuthorityUtils.commaSeparatedStringToAuthorityList(roles));
ptaUser.setUserId(userId);
return new UsernamePasswordAuthenticationToken(ptaUser, null, AuthorityUtils.commaSeparatedStringToAuthorityList(roles));
}
protected boolean requiresAuthentication(HttpServletRequest request) {
return requiresAuthenticationRequestMatcher.matches(request);
}
protected AuthenticationManager getAuthenticationManager() {
return authenticationManager;
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
protected boolean permissiveRequest(HttpServletRequest request) {
if (permissiveRequestMatchers == null)
return false;
for (RequestMatcher permissiveMatcher : permissiveRequestMatchers) {
if (permissiveMatcher.matches(request))
return true;
}
return false;
}
public void setPermissiveUrl(String... urls) {
if (permissiveRequestMatchers == null)
permissiveRequestMatchers = new ArrayList<>();
for (String url : urls)
permissiveRequestMatchers.add(new AntPathRequestMatcher(url));
}
public void setAuthenticationSuccessHandler(
AuthenticationSuccessHandler successHandler) {
Assert.notNull(successHandler, "successHandler cannot be null");
this.successHandler = successHandler;
}
public void setAuthenticationFailureHandler(
AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler, "failureHandler cannot be null");
this.failureHandler = failureHandler;
}
protected AuthenticationSuccessHandler getSuccessHandler() {
return successHandler;
}
protected AuthenticationFailureHandler getFailureHandler() {
return failureHandler;
}
首先通过重写filter对指定的数据进行拦截
通过config配置拦截器的具体请求,这里是将符合的请求(header中携带有anthentication的)拦截下来,接着通过header获取到token
这里获取token的时候,进行了一次jwt签证的校验,通过exception来判断验证的结果
当验证通过的时候,从jwt的payload中读取用户名和权限信息,将用户名和权限信息,放置到UsernameAuthenticationToken容器中,接着将生成好的容器注入到SercurityContextHolder中
这里我们读一下SercurityContextHolder的源码
final class GlobalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static SecurityContext contextHolder;
@Override
public void clearContext() {
contextHolder = null;
}
@Override
public SecurityContext getContext() {
if (contextHolder == null) {
contextHolder = new SecurityContextImpl();
}
return contextHolder;
}
@Override
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
contextHolder = context;
}
@Override
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}
这里通过全局的唯一实例进行注入,所以这次设置之后,在系统的任何地方调用SercurityContextHolder均会获取到用户名和权限信息
这里我认为原来的代码这一部分并没有完全使用spring security,按照security的思路,更适合使用authentication->provider->manager的一整套流程。我会在另一篇播客里写一下思路
Refresh
JWT可以减轻服务器的负担,解决一部分的跨域难题,但是同时,为保证安全性jwt的签发是有一定的时间的,在时间将要过期的时候,我们应当及时的为jwt刷新token
public class JwtRefreshSuccessHandler implements AuthenticationSuccessHandler {
private static final int tokenRefreshInterval = 24; //刷新间隔1天
public JwtRefreshSuccessHandler() {
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String tokenHeader = request.getHeader(JwtUtil.TOKEN_HEADER);
boolean shouldRefresh = shouldTokenRefresh(JwtUtil.getIssuedAt(tokenHeader.replace(JwtUtil.TOKEN_PREFIX, "")));
if (shouldRefresh) {
String roles = authentication.getAuthorities().stream()
.map(Object::toString).collect(Collectors.joining(","));
String newToken = JwtUtil.createToken(authentication.getName(), ((PtaUser) authentication.getPrincipal()).getUserId(), roles);
response.setHeader("Authorization", newToken);
}
}
protected boolean shouldTokenRefresh(Date issueAt) {
LocalDateTime issueTime = LocalDateTime.ofInstant(issueAt.toInstant(), ZoneId.systemDefault());
return LocalDateTime.now().minusHours(tokenRefreshInterval).isAfter(issueTime);
}
}
在项目中采用的是successHandler的方式,当jwt校验成功之后,会自动调用这个handler,完全可以在handler中检验日期是否临近截至日期
在这里,当距离截至日期的时间不足一个tokenRefreshInteval的时候就会触发刷新,刷新的机制也很简单,将刷新有效期之后的token放置到header中
客户端只需要检测是否有authentication就可以判断是不是需要更新本地的token,更新token的操作也可以通过不侵入的拦截器操作进行
controller
controller层只需要通过hasRole注解即可进行
@PostMapping("/getProblemByAuthorId")
@PreAuthorize("hasRole('ROLE_ADMIN') or hasRole('ROLE_TEACHER')")
public ResultEntity getProblemByAuthorId(@RequestParam("page") int page, @RequestParam("size") int size) throws ServletException, IOException {
if (UserUtil.getCurrentUserId() == null) {
return ResultEntity.error(StatusCode.NO_PERMISSION);
}
try {
Pager<Problem> problemPager = problemService.getProblemByAuthorId(UserUtil.getCurrentUserId(), page, size);
return ResultEntity.success("ok", problemPager);
} catch (Exception e) {
log.error("获取题目失败", e);
return ResultEntity.error("error");
}
}
在没有登录,或者是登录时候检测权限的过程中没有发现对应的ADMIN或者TEACHER的,就会被403拦下来,剩下的请求才可以正常的通过进行请求的校验。