shrio笔记
认证
授权
JWT简介(Json Web Token)
1、导入依赖库
<!--shiro--><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-web</artifactId><version>1.5.3</version></dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.5.3</version></dependency><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.10.3</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.11</version></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpcore</artifactId><version>4.4.13</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
2、生成令牌(需要密钥、过期时间、用户id)
emos:jwt:#密钥secret: abc123456#令牌过期时间(天)expire: 5#令牌缓存时间(天数)cache-expire: 10
3、验证令牌的有效性(内容是否有效、是否过期)
创建JwtUtil工具类
package com.example.emoswxapi.config.shiro;import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.emoswxapi.exception.EmosException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import java.util.Date;@Component
@Slf4j
public class JwtUtil {/*
1、导入依赖库
2、生成令牌(需要密钥、过期时间、用户id)
3、验证令牌的有效性(内容是否有效、是否过期)
*///密钥(在配置文件里定义)@Value("${emos.jwt.secret}")private String secret;//过期时间(天)@Value("${emos.jwt.expire}")private int expire;
//userId方法传进来public String createToken(int userId) {Date date = DateUtil.offset(new Date(), DateField.DAY_OF_YEAR, expire).toJdkDate();Algorithm algorithm = Algorithm.HMAC256(secret); //创建加密算法对象JWTCreator.Builder builder = JWT.create();String token = builder.withClaim("userId", userId).withExpiresAt(date).sign(algorithm);return token;}//通过令牌获取userIdpublic int getUserId(String token) {try {DecodedJWT jwt = JWT.decode(token);return jwt.getClaim("userId").asInt();} catch (Exception e) {throw new EmosException("令牌无效");}}//验证令牌的内容和过期public void verifierToken(String token) {Algorithm algorithm = Algorithm.HMAC256(secret); //创建加密算法对象JWTVerifier verifier = JWT.require(algorithm).build();verifier.verify(token);}
}
4、把令牌字符串封装为认证对象
package com.example.emoswxapi.config.shiro;import org.apache.shiro.authc.AuthenticationToken;public class OAuth2Token implements AuthenticationToken {private String token;public OAuth2Token(String token){this.token = token;}@Overridepublic Object getPrincipal() {return token;}@Overridepublic Object getCredentials() {return token;}
}
5、实现认证和授权(创建AuthorizingRealm类的子类,实现认证和授权方法)
package com.example.emoswxapi.config.shiro;import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.util.Set;@Component
public class OAuth2Realm extends AuthorizingRealm {@Autowiredprivate JwtUtil jwtUtil;//传入的令牌是否符合要求@Overridepublic boolean supports(AuthenticationToken token) {return token instanceof OAuth2Token;}/*** 授权(验证权限时调用)*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();//TODO 查询用户的权限列表//TODO 把权限列表添加到info对象中return info;}/*** 认证(登录时调用)*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {//TODO 从令牌中获取userId,然后检测该账户是否被冻结。SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();//TODO 往info对象中添加用户信息、Token字符串return info;}
}
6、如何设计令牌的刷新机制
为什么要刷新令牌?
令牌一旦产生就保存在客户端
即使用户一直在登录使用系统,也不会重新生成令牌
令牌到期,用户必须重新登录
令牌应该自动续期
双令牌机制
设置长短日期的令牌
短日期的令牌失效就用长日期的令牌
令牌缓存机制
令牌缓存到redis上面
缓存的令牌过期时间是客户端令牌的一倍
如果客户端令牌过期,缓存令牌没有过期,则生成新的令牌
如果客户端令牌过期,缓存令牌也过期,则用户必须登录
7、创建存储令的媒介类
package com.example.emoswxapi.config.shiro;import org.springframework.stereotype.Component;
//创建存储令的媒介类
@Component
public class ThreadLocalToken {private ThreadLocal local=new ThreadLocal();public void setToken(String token){local.set(token);}public String getToken(){return (String) local.get();}public void clear(){local.remove();}
}
8、拦截所有请求
创建过滤器
判断哪些请求应该被Shiro处理
option请求直接放行(提交application/json数据,请求被分成options和post两次)
其余所有请求都被Shrio处理
判断Token是真的过期还是假的过期
真过期,返回提示信息,让用户重新登录
假过期,就生成新的令牌,返回给客户端
存储新令牌
ThreadLocalToken
Redis
package com.example.emoswxapi.config.shiro;import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;@Component
@Scope("prototype") //产生多例的对象,不然就是单例的
public class OAuth2Filter extends AuthenticatingFilter {@Autowiredprivate ThreadLocalToken threadLocalToken;@Value("${emos.jwt.cache-expire}")private int cacheExpire;@Autowiredprivate JwtUtil jwtUtil;@Autowiredprivate RedisTemplate redisTemplate;/*** 拦截请求之后,用于把令牌字符串封装成令牌对象*/@Overrideprotected AuthenticationToken createToken(ServletRequest request,ServletResponse response) throws Exception {//获取请求tokenString token = getRequestToken((HttpServletRequest) request);if (StringUtils.isBlank(token)) {return null;}return new OAuth2Token(token);}/*** 拦截请求,判断请求是否需要被Shiro处理*/@Overrideprotected 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处理的请求*/@Overrideprotected 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不存在,直接返回401String 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;}@Overrideprotected 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中获取tokenString token = httpRequest.getHeader("token");//如果header中不存在token,则从参数中获取tokenif (StringUtils.isBlank(token)) {token = httpRequest.getParameter("token");}return token;}@Overridepublic void doFilterInternal(ServletRequest request,ServletResponse response, FilterChain chain) throws ServletException, IOException {super.doFilterInternal(request, response, chain);}
}
9、配置到shrio框架
把Filter和Realm添加到shrio框架
创建四个对象返回给springboot
SecurityManager 用于封装Realm对象
ShrioFilterFactoryBean 用于封装Filter对象 设置Filter拦截路径
LifecycleBeanPostProcessor 管理Shrio对象生命周期
AuthorizationAttributeSourceAdvisor AOP切面类 Web方法执行前,验证权限
package com.example.emoswxapi.config.shiro;import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;@Configuration
public class ShiroConfig {@Bean("securityManager")public SecurityManager securityManager(OAuth2Realm oAuth2Realm) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(oAuth2Realm);securityManager.setRememberMeManager(null);return securityManager;}@Bean("shiroFilter")public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,OAuth2Filter oAuth2Filter) {ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();shiroFilter.setSecurityManager(securityManager);//oauth过滤Map<String, Filter> filters = new HashMap<>();filters.put("oauth2", oAuth2Filter);shiroFilter.setFilters(filters);Map<String, String> filterMap = new LinkedHashMap<>();filterMap.put("/webjars/**", "anon");filterMap.put("/druid/**", "anon");filterMap.put("/app/**", "anon");filterMap.put("/sys/login", "anon");filterMap.put("/swagger/**", "anon");filterMap.put("/v2/api-docs", "anon");filterMap.put("/swagger-ui.html", "anon");filterMap.put("/swagger-resources/**", "anon");filterMap.put("/captcha.jpg", "anon");filterMap.put("/user/register", "anon");filterMap.put("/user/login", "anon");filterMap.put("/test/**", "anon");filterMap.put("/**", "oauth2");shiroFilter.setFilterChainDefinitionMap(filterMap);return shiroFilter;}@Bean("lifecycleBeanPostProcessor")public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();advisor.setSecurityManager(securityManager);return advisor;}}
10、创建AOP切面类
拦截所有的web方法返回值
判断是否刷新生成新令牌(检查THreadLocal中是否保存令牌,把新令牌绑定到R对象中)
package com.example.emoswxapi.aop;import com.example.emoswxapi.common.util.R;
import com.example.emoswxapi.config.shiro.ThreadLocalToken;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;//AOP切面类
@Aspect
@Component
public class TokenAspect {@Autowiredprivate ThreadLocalToken threadLocalToken;@Pointcut("execution(public * com.example.emoswxapi.controller.*.*(..)))")public void aspect() {}//环绕时间,调用的方法可以拦截,返回的值也可以拦截@Around("aspect()")public Object around(ProceedingJoinPoint point) throws Throwable {R r = (R) point.proceed(); //方法执行结果String token = threadLocalToken.getToken();//如果ThreadLocal中存在Token,说明是更新的Tokenif (token != null) {r.put("token", token); //往响应中放置TokenthreadLocalToken.clear();}return r;}
}
11、精简返回给客户端的异常内容
@ControllerAdvice可以全局捕获SpringMVC异常
判断异常的类型 后端数据验证异常
未授权异常 你不具有相关权限
EmosException 精简异常内容
普通异常 后端执行异常
package com.example.emoswxapi.config;import com.example.emoswxapi.exception.EmosException;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;//精简返回异常
@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {@ResponseBody@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)@ExceptionHandler(Exception.class)public String validExceptionHandler(Exception e) {log.error("执行异常",e);if (e instanceof MethodArgumentNotValidException) {MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;//将错误信息返回给前台return exception.getBindingResult().getFieldError().getDefaultMessage();}else if(e instanceof EmosException){EmosException exception=(EmosException)e;return exception.getMsg();}else if(e instanceof UnauthorizedException){return "你不具有相关权限";}else {return "后端执行异常";}}
}