流程:
1、将token(JWT生成和验证)封装成认证对象(使用ThreadLocal保证线程安全)
2、定义认证与授权的实现方法(Realm类)
3、拦截HTTP请求,验证Token(Filter)
4、把设置应用到Shiro框架(创建ShiroConfig回传四个对象)
5、 回传token:使用AOP拦截Web对象返回方法,从ThreadLocalToken中获取token写入返回对象,然后返回。
代码实现
JWT加密和验证token
/**
* JWT对userId进行加密生成token
* 生成token、从token中获得userId、验证token的合法性
*/
@Component
public class JwtUtil {
@Value("${emos.jwt.secret}")
private String secret;
@Value("${emos.jwt.expire}")
private int expire;
@Value("${emos.jwt.cache-expire}")
private String cacheExpire;
//使用userId创建token
public String creatToken(int userId){
//计算偏移5天(expire)后的数据
Date date=DateUtil.offset(new Date(), DateField.DAY_OF_YEAR,expire);
//创建算法对象
Algorithm algorithm=Algorithm.HMAC256(secret);
JWTCreator.Builder builder= JWT.create();
//加密
String token=builder.withClaim("userId",userId).withExpiresAt(date).sign(algorithm);
return token;
}
//从token中获取userId
public int getUserId(String token){
//创建一个解码对象
DecodedJWT jwt=JWT.decode(token);
int userId=jwt.getClaim(token).asInt();
return userId;
}
//验证token是否合法
public void verifierToken(String token){
//创建算法对象
Algorithm algorithm=Algorithm.HMAC256(secret);
//构造验证对象
JWTVerifier verifier=JWT.require(algorithm).build();
//验证token,是不是是否过期,是不是使用secret密钥进行加密的
verifier.verify(token);//验证失败抛出RuntimException,所以不需要返回,直接捕获异常即可
}
}
一、封装token
@Component
public class ThreadLocalToken {
private ThreadLocal<String> tokenLocal=new ThreadLocal<>();
public void setTokenLocal(String token){
tokenLocal.set(token);
}
public String getTokenLocal(){
return tokenLocal.get();
}
public void clear(){
tokenLocal.remove();
}
}
二、创建Realm类继承AuthorizingRealm ,实现授权和认证
//shiro授权,验证token
@Component
public class AuthRealm extends AuthorizingRealm {
@Autowired
private JwtUtil jwtUtil;
/**
* 判断传入的token对象是否符合要求
* AuthenticationToken是接口类型,传入AuthenticationToken的子类即可,判断是否是自定义的AuthToken类对象
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof AuthToken;
}
/**
* 授权(验证权限时调用)
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo();
//TODO 查询用户的权限列表
//TODO 把权限列表添加到info对象中
return (AuthorizationInfo) info;
}
/**
* 认证(登录时调用)
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//TODO 从令牌中获取userId,然后检测该账户是否被冻结
SimpleAuthenticationInfo info=new SimpleAuthenticationInfo();
//TODO 往info对象中添加用户信息、Token字符串
return info;
}
}
三、创建Filter继承AuthenticatingFilter 对HTTP请求进行过滤,并对token进行验证
/**
* 对请求进行过滤,对Options请求放行
* 封装token对象
*/
@Component
@Scope("prototype")
public class AuthFilter extends AuthenticatingFilter {
@Autowired
private ThreadLocalToken threadLocalToken;
@Value("${emos.jwt.cache-expire}")
private int cacheExpire;
@Autowired
private JwtUtil jwtUtil; //验证token
@Autowired
private RedisTemplate redisTemplate; //操作Redis
/**
* 拦截请求后,用于把令牌字符串封装成令牌对象AuthToken
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
//获取请求token
String token=getRequestToken((HttpServletRequest) servletRequest);
if (StrUtil.isBlank(token)){ //判断Token是否为null和空字符串
return null;
}
return new AuthToken(token);
}
/**
* 拦截请求,判断是否需要被Shiro处理,放行Options请求
* 返回为true则不执行onAccessDenied,反之执行
* @param request
* @param response
* @param mappedValue
* @return true 放行 false 不放行
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest req= (HttpServletRequest) request;
//Ajaxt提交application/json数据的时候,会先发出Options请求(判断连接是否可用,不携带数据)
//这里要放行Options请求,不需要Shiro处理
if(req.getMethod().equals(RequestMethod.OPTIONS.name())){
return true;
}
//除了Options请求外,都不予放行,等待被Shiro处理
return false;
}
/**
* 从请求头中获取token,验证token是否有效,是否需要刷新
* false 请求结束
* true 进入到业务controller
* 核心: 调用executeLogin, 并不是真正的login, 本质还是调用subject.login(token)到Realm去做认证, 返回true认证通过, 访问controller
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request= (HttpServletRequest) servletRequest;
//设置响应体参数
HttpServletResponse response= (HttpServletResponse) servletResponse;
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
//允许跨域请求
response.setHeader("Access-Control-Allow-Credentials","true");
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
//使用前先清空
threadLocalToken.clear();
String token=getRequestToken(request);
if (StrUtil.isBlank(token)){ //token为空返回客户端一个错误消息
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
response.getWriter().print("无效令牌");
return false;
}
//验证token
try{
jwtUtil.verifierToken(token);//验证失败抛出异常
}catch (TokenExpiredException e){//过期异常,刷新令牌
//如果客户端token过期,判断redis中的缓存异常是否过期
//缓存异常过期则需要重新登录,未过期就重新生成token
if(redisTemplate.hasKey(token)){ //缓存token未过期
redisTemplate.delete(token);
//从老token中获取userId重新加密生成新token,并添加到redis和ThreadLocal中
int userId=jwtUtil.getUserId(token);
token=jwtUtil.creatToken(userId);
redisTemplate.opsForValue().set(token,userId+"",cacheExpire, TimeUnit.DAYS);
threadLocalToken.setTokenLocal(token);
}
else{ //两者都过期,需要重新登录
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
response.getWriter().print("令牌已过期");
return false;
}
}catch (JWTDecodeException e){//内容异常
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
response.getWriter().print("无效令牌");
return false;
}
//令牌正常,执行Realm类,进行Shiro的认证和授权
//调用executeLogin, 并不是真正的login, 本质还是调用subject.login(token)到Realm去做认证, 返回true认证通过, 访问controller
return executeLogin(servletRequest,servletResponse);//认证和授权失败都返回false
}
/**
* executeLogin 认证失败调用方法
* @param token
* @param e
* @param request
* @param response
* @return
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
//认证失败返回状态码等响应信息
HttpServletResponse resp= (HttpServletResponse) request;
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
//允许跨域请求
resp.setHeader("Access-Control-Allow-Credentials","true");
resp.setHeader("Access-Control-Allow-Origin", resp.getHeader("Origin"));
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
try {
resp.getWriter().print(e.getMessage());//返回错误信息
} catch (Exception exception) {
}
return false;
}
@Override
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
super.doFilterInternal(request, response, chain);
}
/**
* 从请求头中获取token
* @param request
* @return
*/
private String getRequestToken(HttpServletRequest request){
String token=request.getHeader("token");
if (StrUtil.isBlank(token)){ //如果请求头中获取不到token就尝试从请求体中获得
token=request.getParameter("token");
}
return token;
}
}
四、创建ShiroConfig,将Realm和Filter写入Shiro框架
@Configuration
public class ShiroConfig {
/**
* 封装Realm对象
* @param realm
* @return
*/
@Bean("securityManager")
public SecurityManager securityManager(AuthRealm realm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setRememberMeManager(null);
return securityManager;
}
/**
* 封装Filter对象
* 设置Filter拦截路径
* @param securityManager
* @param filter
* @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,AuthFilter filter){
ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//将Filter对象封装到Map中,然后map传给facteryBean
Map<String, Filter> map=new HashMap<>();
map.put("authc",filter); //authc--拦截
shiroFilter.setFilters(map);
//过滤规则
Map<String,String> mapFilter=new LinkedHashMap<>();
//TODO 拦截规则待更新
//anno表示允许匿名访问
mapFilter.put("/webjars/**","anno");
mapFilter.put("/druid/**","anno");
mapFilter.put("/app/**","anno");
mapFilter.put("/sys/login","anno");
mapFilter.put("/swagger-ui.html","anno");
mapFilter.put("/druid/**","anno");
mapFilter.put("/v2/api-docs","anno");
mapFilter.put("/user/register","anno");
mapFilter.put("/user/login","anno");
mapFilter.put("/test/**","anno");
mapFilter.put("/**","authc");//authc--进行身份认证后才能访问
shiroFilter.setFilterChainDefinitionMap(mapFilter);
return shiroFilter;
}
/**
* 管理Shiro对象生命周期
* @return
*/
@Bean()
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
/**
* 开启Shiro注解模式,可以在Controller中的方法上添加注解
* AOP切面类,Web方法执行前,验证权限
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
五、创建TokenAop拦截Web方法返回对象,传递给客户端新的token
/**
* 拦截web方法的返回值,对token进行处理
*/
@Component
@Aspect
public class TokenAop {
@Autowired
private ThreadLocalToken threadLocalToken;
@Pointcut("execution(public * com.example.emoswx.controller.*.*(..))")
public void aspect(){
}
@Around("aspect()") //环绕通知
//通过ProceedingJoinPoint获取当前执行的方法
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
ReturnMap returnMap = (ReturnMap) joinPoint.proceed();//执行目标方法,因为controller中的方法返回值被封装成R对象,所以可以使用R对象接收
String token=threadLocalToken.getTokenLocal();
if (token!=null){ //判断是否生成新的token,生成则返回给客户端
returnMap.put("token",token);
//每次使用完ThreadLocal都调用它的remove()方法清除数据,防止内存泄露
threadLocalToken.clear();
}
return returnMap;
}
}