持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第8天,点击查看活动详情
Shiro集成JWT实现认证与授权
Shiro处理事务的流程
- 客户端携带token字符串返回给客户端。
- AuthenticatingFilter对需要拦截的Http请求进行拦截,并获取请求中的 Token。
- 把得到的token字符串通过AuthenticationToken封装成AuthenticationToken对象。
- 封装后的token对象传入到AuthorizingRealm中,以便于认证和授权。
封装AuthenticationToken类型的对象
在上一篇文章中已经创建JWT工具类,Shiro框架会拦截所有的Http请求,校验token的有效性。但是token并不能直接提交给Shiro框架,需要封装成AuthenticationToken类型的对象。所以要先创建实现类。
public class OAuth2Token implements AuthenticationToken {
private String token;
public OAuth2Token(String token){
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
实现AuthorizingRealm的实现类
@Component
public class OAuth2Realm extends AuthorizingRealm {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof OAuth2Token;
}
/**
* 授权(验证权限时调用)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//TODO 查询用户的权限列表
//TODO 把权限列表添加到info对象中
return info;
}
/**
* 认证(登录时调用)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//TODO 从令牌中获取userId,然后检测该账户是否被冻结。
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();
//TODO 往info对象中添加用户信息、Token字符串
return info;
}
}
实现AuthenticatingFilter的实现类
创建ThreadLocalToken类
用于保存用户的Token信息,方便后续在AOP中,对Token进行自动刷新功能。
在配置文件中添加配置
emos:
jwt:
#密钥
secret: abc123456
#令牌过期时间(天)
expire: 5
#令牌缓存时间(天数)
cache-expire: 10
AuthenticatingFilter的实现方法
但是还不能起作用,需要使用ShiroConfig将OAuth2Filter和OAuth2Realm配置到Shiro框架中。
@Component
@Scope("prototype")
public class OAuth2Filter extends AuthenticatingFilter {
@Autowired
private ThreadLocalToken threadLocalToken;
@Value("${emos.jwt.cache-expire}")
private int cacheExpire;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RedisTemplate redisTemplate;
/**
* 拦截请求之后,用于把令牌字符串封装成令牌对象
*/
@Override
protected AuthenticationToken createToken(ServletRequest request,
ServletResponse response) throws Exception {
//获取请求token
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
return null;
}
return new OAuth2Token(token);
}
/**
* 拦截请求,判断请求是否需要被Shiro处理
*/
@Override
protected boolean isAccessAllowed(ServletRequest request,
ServletResponse response, Object mappedValue) {
HttpServletRequest req = (HttpServletRequest) request;
// Ajax提交application/json数据的时候,会先发出Options请求
// 这里要放行Options请求,不需要Shiro处理
if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
// 除了Options请求之外,所有请求都要被Shiro处理
return false;
}
/**
* 该方法用于处理所有应该被Shiro处理的请求
*/
@Override
protected boolean onAccessDenied(ServletRequest request,
ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
resp.setHeader("Content-Type", "text/html;charset=UTF-8");
//允许跨域请求
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
threadLocalToken.clear();
//获取请求token,如果token不存在,直接返回401
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("无效的令牌");
return false;
}
try {
jwtUtil.verifierToken(token); //检查令牌是否过期
} catch (TokenExpiredException e) {
//客户端令牌过期,查询Redis中是否存在令牌,如果存在令牌就重新生成一个令牌给客户端
if (redisTemplate.hasKey(token)) {
redisTemplate.delete(token);//删除令牌
int userId = jwtUtil.getUserId(token);
token = jwtUtil.createToken(userId); //生成新的令牌
//把新的令牌保存到Redis中
redisTemplate.opsForValue().set(token, userId + "", cacheExpire, TimeUnit.DAYS);
//把新令牌绑定到线程
threadLocalToken.setToken(token);
} else {
//如果Redis不存在令牌,让用户重新登录
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("令牌已经过期");
return false;
}
} catch (JWTDecodeException e) {
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("无效的令牌");
return false;
}
boolean bool = executeLogin(request, response);
return bool;
}
@Override
protected boolean onLoginFailure(AuthenticationToken token,
AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.setContentType("application/json;charset=utf-8");
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
try {
resp.getWriter().print(e.getMessage());
} catch (IOException exception) {
}
return false;
}
/**
* 获取请求头里面的token
*/
private String getRequestToken(HttpServletRequest httpRequest) {
//从header中获取token
String token = httpRequest.getHeader("token");
//如果header中不存在token,则从参数中获取token
if (StringUtils.isBlank(token)) {
token = httpRequest.getParameter("token");
}
return token;
}
@Override
public void doFilterInternal(ServletRequest request,
ServletResponse response, FilterChain chain) throws ServletException, IOException {
super.doFilterInternal(request, response, chain);
}
}