1、什么是Shiro
Shiro是Java畛域十分出名的认证( Authentication )与受权 ( Authorization )框架,用以代替JavaEE中的JAAS性能。相较于其余认证与受权框架,Shiro设计的非常简单,所以广受好评。任意JavaWeb我的项目都能够应用Shiro框架,而Spring Security 必须要应用在Spring我的项目中。所以Shiro的适用性更加宽泛。像什 么 JFinal 和 Nutz 非Spring框架都能够应用Shiro,而不能应用Spring Security框架。
- 两大性能: 认证 与 受权
- 实用于JavaWeb我的项目
- 不是Spring框架下的产品
1.1 什么是认证和受权
- 认证:认证就是要核验用户的身份,比如说通过用户名和明码来测验用户的身份。说简略一些,认证就
是登陆。登陆之后Shiro要记录用户胜利登陆的凭证。
- 受权:受权是比认证更加精密度的划分用户的行为。比如说在咱们开发的零碎中,对于数据库一般的开发人员可能只有读写权限,而数据库管理员能够有读写删等更高级的权限。这就是利用受权来限定不同身份用户 的行为。
1.2 shrio怎么上述两个性能?
Shiro能够利用 HttpSession 或者 Redis 存储用户的登陆凭证,以及角色或者身份信息。而后利用过滤器(Filter),对每个Http申请过滤,查看申请对应的HttpSession 或者 Redis 中的认证与受权信息。如果用户没有登陆,或者权限不够,那么Shiro会向客户端返回错误信息。 也就是说,咱们写用户登陆模块的时候,用户登陆胜利之后,要调用Shiro保留登陆凭证。而后查问用户的角色和权限,让Shiro存储起来。未来不论哪个办法须要登陆拜访,或者领有特定的角色 跟权限能力拜访,咱们在办法前设置注解即可,非常简单。
2、什么是JWT
JWT(Json Web Token), 是为了在网络应用环境间传递申明而执行的一种基于JSON的凋谢标 准。JWT个别被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也能够减少一些额定的其它业务逻辑所必须的申明信息,该token也可间接被用于认证,也可被加密。
- JWT能够用在单点登录的零碎中
如果用户的登陆凭证通过加密( Token )保留在客户端,客户端每次提交申请的时候,把 Token 上传给后端服务器节点。即使后端我的项目应用了负载平衡,每个后端节点接管到客户端上传 的Token之后,通过检测,是无效的 Token ,于是就判定用户曾经胜利登陆,接下来就能够提供后端服务了
- JWT兼容更多的客户端
传统的 HttpSession 依附浏览器的 Cookie 寄存 SessionId ,所以要求客户端必须是浏览器。当初的JavaWeb零碎,客户端能够是浏览器、APP、小程序,以及物联网设施。为了让这些设施都能拜访到JavaWeb我的项目,就必须要引入JWT技术。JWT的 Token 是纯字符串,至于客户端怎么保留,没有具体要求。只有客户端发动申请的时候,附带上 Token 即可。所以像物联网设施,我 们能够用 SQLite 存储 Token 数据。
3、JWT的实战
通过下面的介绍,咱们曾经理解了JWT的相干信息。说白了,JWT就是对咱们的token进行加密,保留在客户端,每一个客户端的申请过去咱们就对这个token进行校验,来判断用户的信息
3.1 创立JWT工具类
用于对Token进行加密,解密,生成...
- 导入依赖
<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>
- 为了保护的不便,咱们能够给token在配置文件中定义好过期工夫,秘钥,缓存工夫。前面通过属性注入即可
- 创立JWT工具类
@Component@Slf4jpublic class JWTUtil { @Value("${emos.jwt.secret}") private String secret; @Value("${emos.jwt.expire}") private int expire; /** * 通过UserId,创立一个token * @param userId * @return */ public String createToken(int userId){ DateTime offset = DateUtil.offset(new Date(), DateField.DAY_OF_YEAR, expire); Algorithm algorithm = Algorithm.HMAC256(secret);//应用算法加密secret密钥 JWTCreator.Builder builder = JWT.create(); String token = builder.withClaim("userId", userId) .withExpiresAt(offset) .sign(algorithm); return token; } /** * 通过token获取userId * @param token * @return */ public int getUserId(String token){ DecodedJWT decodedJWT = JWT.decode(token); int userId = decodedJWT.getClaim("userId").asInt(); return userId; } /** *验证密钥 * @param token */ public void verifierToken(String token){ Algorithm algorithm =Algorithm.HMAC256(secret);//对密钥进行加密算法解决 JWTVerifier verifier = JWT.require(algorithm).build();//创立一个jwt的verifier对象 verifier.verify(token);//验证密钥和token(加密后的密钥局部)是否统一---sign }}
4、对接JWT和Shiro框架
当初咱们能够用过JWTUtil工具类生成token并对其进行一些操作,而token是不能间接交给Shiro的,依照Shiro框架,咱们还须要对token进行封装。
4.1 封装token对象
public class OAuth2Token implements AuthenticationToken { private String token; /** * 创立生成token对象的结构器 * * @param token */ public OAuth2Token(String token){ this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; }}
4.2 创立Realm类
依据下面实现shiro框架的步骤,咱们须要创立OAuth2Realm类,这个类须要继承AuthorizingRealm
OAuth2Realm 类是 AuthorizingRealm 的实现类,咱们要在这个实现类中定义认证和受权的办法。因为认证与受权模块设计到用户模块和权限模块,当初咱们还没有真正的开发业务模块,所 以咱们这里先临时定义空的认证去受权办法,把Shiro和JWT整合起来,在后续章节咱们再实现 认证与受权。
@Componentpublic class OAuth2Realm extends AuthorizingRealm { @Override public boolean supports(AuthenticationToken token) { return token instanceof OAuth2Token; } /** * 受权(验证权限时调用) */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //TODO 查问用户的权限列表 //TODO 把权限列表增加到info对象中 return info; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //TODO 从令牌中获取userId,而后检测该账户是否被解冻。 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(); //TODO 往info对象中增加用户信息、Token字符串 return info; }}
4.3 如何设计token(令牌)的过期工夫
咱们在定义JwtUtil工具类的时候,生成的 Token 都有过期工夫。那么问题来了,假如 Token 过 期工夫为15天,用户在第14天的时候,还能够免登录失常拜访零碎。然而到了第15天,用户的 Token过期,于是用户须要从新登录零碎。
HttpSession 的过期工夫比拟优雅,默认为15分钟。如果用户间断应用零碎,只有间隔时间不超 过15分钟,零碎就不会销毁 HttpSession 对象。
JWT的令牌过期工夫能不能做成 HttpSession 那样超时工夫,只有用户距离操作工夫不超过15天,零碎就不须要用户从新登录零碎。实现这种 成果的计划有两种: 双Token 和 Token缓存 ,这里重点讲一下 Token 缓存计划。
简略的来说,就是当咱们第一次登录的时候,此时咱们无效的登录工夫是15天,如果我在第15天的时候登录,那么此时还不应该须要从新登录,只有说在第15天过后,也就是咱们15天内没有操作,且再过15天之后过期,这就是为什么咱们缓存的工夫是过期工夫的1倍。
Token缓存计划是把 Token 缓存到Redis,而后设置Redis外面缓存的 Token 过期工夫为失常 Token 的1倍,而后依据状况刷新 Token 的过期工夫。
Token生效,缓存也不存在的状况:当第15天,用户的 Token 生效当前,咱们让Shiro程序到Redis查看是否存在缓存的 Token ,如果这个 Token 不存在于Redis外面,就阐明用户的操作距离了15天,须要从新登录。
Token生效,然而缓存还存在的状况 如果Redis中存在缓存的 Token ,阐明以后 Token 生效后,间隔时间还没有超过15天,不应该让用户从新登录。所以要生成新的 Token 返回给客户端,并且把这个 Token 缓存到Redis外面,这种操作称为刷新 Token 过期工夫。
咱们定义 OAuth2Filter 类拦挡所有的HTTP申请:
一方面它会把申请中的 Token 字符串提取出 来,封装成对象交给Shiro框架;
另一方面,它会查看 Token 的有效性。如果 Token 过期,那么 会生成新的 Token ,别离存储在 ThreadLocalToken 和 Redis 中。
之所以要把 新令牌 保留到 ThreadLocalToken 外面,是因为要向 AOP切面类 传递这个 新令牌 。 尽管 OAuth2Filter 中有 doFilterInternal() 办法,咱们能够失去响应并且写入 新令牌 。然而 这个做十分麻烦,首先咱们要通过IO流读取响应中的数据,而后还要把数据解析成JSON对象, 最初再放入这个新令牌。如果咱们定义了 AOP切面类 ,拦挡所有Web办法返回的 R对象 ,而后 在 R对象 外面增加 新令牌 ,这多简略啊。然而 OAuth2Filter 和 AOP 切面类之间没有调用关 系,所以咱们很难把 新令牌 传给 AOP切面类 。 这里我想到了 ThreadLocal ,只有是同一个线程,往 ThreadLocal 外面写入数据和读取数据是 完全相同的。在Web我的项目中,从 OAuth2Filter 到 AOP切面类 ,都是由同一个线程来执行的,中途不会更换线程。所以咱们能够释怀的把新令牌保留都在 ThreadLocal 外面, AOP切面类 能够成 功的取出新令牌,而后往 R对象 外面增加新令牌即可。 ThreadLocalToken 是我自定义的类,外面蕴含了 ThreadLocal 类型的变量,能够用来保留线程 平安的数据,而且防止了应用线程锁
4.4 设计ThreadLocalToken类
@Componentpublic class ThreadLocalToken { private ThreadLocal<String> threadLocal; public void setToken(String token){ threadLocal.set(token); } public String getToken(){ return threadLocal.get(); } public void clear(){ threadLocal.remove(); }}
4.5 创立OAuth2Filter类
@Component@Scope("prototype")//在spring的IOC容器中应用多例模式创立实例public class OAuth2Filter extends AuthenticatingFilter { @Autowired private RedisTemplate redisTemplate;//将token保留到redis @Autowired private ThreadLocalToken threadLocalToken;//保留token的封装类 @Value("${emos.jwt.cache-expire}") private int expireDate;//令牌过期工夫 @Autowired private JWTUtil jwtUtil; //用于令牌加密的工具类 /** * 生成token * @param servletRequest * @param servletResponse * @return * @throws Exception */ @Override protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { HttpServletRequest request = (HttpServletRequest) servletRequest; //从申请中获取token并进行判断 String tokenStr = getTokenByRequest(request); if(StrUtil.isBlank(tokenStr)){ return null; } return new OAuth2Token(tokenStr); } /** * 是否容许过滤 * @param request * @param response * @param mappedValue * @return */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {// 判断是否是Option申请 HttpServletRequest req = (HttpServletRequest) request; if(req.getMethod().equals(HttpMethod.OPTIONS.name())){ return true;//放过OPTIONS申请 } return false; } /** * 解决所有应该有shiro解决的申请 * 也就是不被放过的申请 * @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.setHeader("Content-Type","text/html;charset=UTF-8"); //容许跨域申请 response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Origin", response.getHeader("Origin")); //清空以后ThreadLocalToken中的token,因为咱们前面可能要新生成token threadLocalToken.clear(); //从申请中获取令牌 String tokenStr = getTokenByRequest(request); //如果令牌为空,在响应中返回前端相干信息 if(StrUtil.isBlank(tokenStr)){ response.setStatus(HttpStatus.SC_UNAUTHORIZED); response.getWriter().print("no useful token"); return false; } //如果令牌存在,刷新令牌 try{ //校验令牌 jwtUtil.verifierToken(tokenStr); }catch (TokenExpiredException e) { //发现令牌过期 //去redis中找 if (redisTemplate.hasKey("token")) { //redis存在,从新为客户生成一个新的token redisTemplate.delete("token"); int userId = jwtUtil.getUserId(tokenStr); String token = jwtUtil.createToken(userId); redisTemplate.opsForValue().set(token, userId + "", expireDate, TimeUnit.DAYS); threadLocalToken.setToken(token); } //redis令牌不存在,须要从新登录 else { response.setStatus(HttpStatus.SC_UNAUTHORIZED); response.getWriter().print("令牌曾经过期"); return false; } }catch (JWTDecodeException e){ response.setStatus(HttpStatus.SC_UNAUTHORIZED); response.getWriter().print("有效的令牌"); return false; } boolean bool = executeLogin(request, response); return bool; } /** * 解决登录失败的状况 * @param token * @param e * @param request * @param response * @return */ @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; } private String getTokenByRequest(HttpServletRequest httpServletRequest){ String token = httpServletRequest.getHeader("token"); if(StrUtil.isBlank(token)){ //如果申请头中不存在就去申请体中获取 token = httpServletRequest.getParameter("token"); } return token; }}
4.6 创立ShiroConfig类
@Configurationpublic 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 shiroFilterFactoryBean(SecurityManager securityManager, OAuth2Filter oAuth2Filter) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); HashMap<String, Filter> filters = new HashMap<>(); filters.put("oauth2", oAuth2Filter); shiroFilter.setFilters(filters); //因为LinkedHashMap能够保障存获得有序性 LinkedHashMap<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 public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){ return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; }}
4.7 利用AOP,把更新的令牌返回给客户端
@Aspect@Componentpublic class tokenAop { @Autowired private ThreadLocalToken localToken; @Pointcut("execution(public * com.example.emos.wx.controller.*.*(..)))") public void aspect(){ } @Around("aspect()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ R r = (R) proceedingJoinPoint.proceed(); String token = localToken.getToken(); if(token != null){ r.put("token",token); localToken.clear(); } return r; }}
这样就实现了咱们Jwt+Shiro和咱们我的项目的集成!!