序言
断更的这几天,
一是公司的事情稍微有点多,回家晚一点就只看了看Vue的视频,
二是,在研究security如何使用token,以及oss的一些东西(新添加了一个文件模块)。
也是乘着今天周六,花了一上午把基于session的方式改为了基于token、封装redis工具类、token工具类。
下午也没干啥,睡了一觉、玩了一把大乱斗,拿快递吃饭。
Vue的话,今天能看到50p,一共170P,按照这个进度还需要两三周。
等看到vue脚手架的时候,我应该就会开始搭建前端的框架了
XpStart–2022.4.16
新增了一个文件模块
打算先整合oss,不过现在对于oss过期时间访问还研究得不是很明白
实现Security + jwt
实现了security+token的方式。
我想的是就不采用以配置的方式来决定使用session还是token了。直接使用token,毕竟session有局限性
使用自定义的token过滤器继承BasicAuthenticationFilter
public class JwtTokenFilter extends BasicAuthenticationFilter {
@Autowired
private TokenUtil tokenUtil;
@Autowired
private RedisUtil redisUtil;
public JwtTokenFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 获取请求头的token
String token = request.getHeader(tokenUtil.getHeader());
// token为空,有可能是访问放行或可匿名的资源,让它继续走
if (StringUtils.isEmpty(token)) {
chain.doFilter(request, response);
return;
}
// 验证token合法性,并取出值
try {
tokenUtil.verifyToken(token);
String userName = tokenUtil.getUserName(token);
String key = RedisKeyPrefixConstants.USER_TOKEN_PREFIX + userName;
if (! redisUtil.hasKey(key)) {
// token 失效,请重新登录
ResponseData error = ResponseData.error("身份过期,请重新登录", HttpStatusConstants.UNAUTHORIZED);
String s = new ObjectMapper().writeValueAsString(error);
response.setCharacterEncoding("utf-8");
response.getWriter().print(s);
response.setStatus(org.springframework.http.HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
}
else if (token.equals(redisUtil.getValue(key, new String()))){
// todo 通过id去查询权限
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request, response);
}
} catch (Exception e) {
// token 无效
ResponseData error = ResponseData.error("Invalid Token", HttpStatusConstants.UNAUTHORIZED);
String s = new ObjectMapper().writeValueAsString(error);
response.setCharacterEncoding("utf-8");
response.getWriter().print(s);
response.setStatus(org.springframework.http.HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
}
chain.doFilter(request, response);
}
}
TokenUtil:
我这里使用的是java-jwt
的依赖,可能和jjwt
的api有些不一样
@Component
public class TokenUtil {
@Value("${token.header}")
private String header;
@Value("${token.secret}")
private String secret;
@Value("${token.expire}")
private int expire = 24 * 60;
/**
* 创建token
* @param userName
* @return
*/
public String createToken(String userName) {
// 添加荷载,即要保存到token的用户信息等
JWTCreator.Builder builder = JWT.create();
// 设置过期时间
Calendar instance = Calendar.getInstance();
instance.add(Calendar.MINUTE, expire);
// 返回token
return builder.withExpiresAt(instance.getTime()).withSubject(userName).sign(Algorithm.HMAC256(secret));
}
/**
* 验证token,如果token非法,会抛出异常
* @param token
*/
public void verifyToken(String token) {
JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
}
/**
* 获取token中保存的信息(如果你保存了的话),token非法会抛出异常
* @param token
* @return
*/
public TokenInfo getInfo(String token) {
DecodedJWT verify = JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
String id = verify.getClaim("id").asString();
String userName = verify.getClaim("userName").asString();
TokenInfo tokenInfo = new TokenInfo();
tokenInfo.setId(id);
tokenInfo.setUserName(userName);
return tokenInfo;
}
/**
* 验证token是否过期
* @param token
* @return true表示过期,false没过期
*/
public boolean isExpire(String token) {
try {
Date expiresAt = JWT.decode(token).getExpiresAt();
Date now = new Date();
if (expiresAt.before(now)) {
return false;
}
} catch (JWTDecodeException e) {
return true;
}
return true;
}
/**
* 获取主体,即用户名
* @param token
* @return
*/
public String getUserName(String token) {
return JWT.decode(token).getSubject();
}
public String getHeader() {
return header;
}
public int getExpire() {
return expire;
}
}
因为RedisUtil的方法并没有全面测试过,所以这里先不放出来,等后面没问题了再加上。
我在用户登录成功的处理器中,将生成的token存放在了redis中
为什么要存放在redis中?
设想一下,用户A,在多个浏览器上登录或者一个浏览器上一直登录,那么每登录一次,服务端就会创建一个token,这是不合理的。因此,存放在redis中,可以控制一个用户一个token,也可以用于单点登录。
还有就是,对于一个不友好的用户,我们从系统中将其拉黑或者删除,将token存放在redis中,可以直接让这个token失效。而不会导致非法用户拥有合法的token。用户修改密码的情况同样适用于此。
自定义认证异常处理
对于认证异常,我们只需要告诉"认证失败"或者"登录失败"
@Component
public class AuthenticationException implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.AuthenticationException authException) throws IOException, ServletException {
ResponseData result = ResponseData.error("认证失败", HttpStatus.UNAUTHORIZED.value());
String s = new ObjectMapper().writeValueAsString(result);
response.setCharacterEncoding("utf-8");
response.getWriter().print(s);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
}
}
对于拒绝访问异常,只需要告诉权限不足
@Component
public class AccessHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseData result = ResponseData.error("权限不足", HttpStatus.FORBIDDEN.value());
String s = new ObjectMapper().writeValueAsString(result);
response.setCharacterEncoding("utf-8");
response.getWriter().print(s);
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
}
}
好了,继续学习vue去了
修改记录:
2022.4.22:
修改了JwtTokenFilter,昨晚发现之前的token过滤器会拦截到放行的资源。
在JwtTokenFilter中,应该首先判断token为不为空。为空说明访问的可能是放行资源、可匿名访问资源。token为空直接执行下一个过滤器,方法应该直接return结束不做任何处理