Shiro靠什么做认证与授权的?
Shiro可以利用HttpSession
或者Redis
存储用户的登陆凭证,以及角色或者身份信息。然后利用过滤器(Filter),对每个Http请求过滤,检查请求对应的HttpSession
或者Redis
中的认证与授权信息。如果用户没有登陆,或者权限不够,那么Shiro会向客户端返回错误信息。
也就是说,我们写用户登陆模块的时候,用户登陆成功之后,要调用Shiro保存登陆凭证。然后查询用户的角色和权限,让Shiro存储起来。将来不管哪个方法需要登陆访问,或者拥有特定的角色跟权限才能访问,我们在方法前设置注解即可,非常简单
WT可以用在单点登录的系统中
传统的JavaWeb
项目,利用HttpSession
保存用户的登陆凭证。如果后端系统采用了负载均衡设计,当用户在A节点成功登陆,那么登陆凭证保存在A节点的HttpSession
中。如果用户下一个请求被负载均衡到了B节点,因为B节点上面没有用户的登陆凭证,所以需要用户重新登录,这个体验太糟糕了。
如果用户的登陆凭证经过加密(Token
)保存在客户端,客户端每次提交请求的时候,把Token
上传给后端服务器节点。即便后端项目使用了负载均衡,每个后端节点接收到客户端上传的Token之后,经过检测,是有效的Token
,于是就断定用户已经成功登陆,接下来就可以提供后端服务了
JWT兼容更多的客户端
传统的HttpSession
依靠浏览器的Cookie
存放SessionId
,所以要求客户端必须是浏览器。现在的JavaWeb系统,客户端可以是浏览器、APP、小程序,以及物联网设备。为了让这些设备都能访问到JavaWeb项目,就必须要引入JWT技术。JWT的Token
是纯字符串,至于客户端怎么保存,没有具体要求。只要客户端发起请求的时候,附带上Token
即可。所以像物联网设备,我们可以用SQLite
存储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>
package com.example.emos.wx.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.emos.wx.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 { //密钥 @Value("${emos.jwt.secret}") private String secret; //过期时间(天) @Value("${emos.jwt.expire}") private int expire; 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; } public 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); } }
客户端提交的Token不能直接交给Shiro框架,需要先封装成AuthenticationToken
类型的对象,所以我们我们需要先创建AuthenticationToken
的实现类。
package com.example.emos.wx.config.shiro; import org.apache.shiro.authc.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; } }
OAuth2Realm
类是AuthorizingRealm
的实现类,我们要在这个实现类中定义认证和授权的方法。因为认证与授权模块设计到用户模块和权限模块,现在我们还没有真正的开发业务模块,所以我们这里先暂时定义空的认证去授权方法,把Shiro和JWT整合起来,在后续章节我们再实现认证与授权。
package com.example.emos.wx.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 { @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; } }
一、为什么要刷新Token的过期时间?
我们在定义JwtUtil工具类的时候,生成的Token
都有过期时间。那么问题来了,假设Token
过期时间为15天,用户在第14天的时候,还可以免登录正常访问系统。但是到了第15天,用户的Token过期,于是用户需要重新登录系统。
HttpSession
的过期时间比较优雅,默认为15分钟。如果用户连续使用系统,只要间隔时间不超过15分钟,系统就不会销毁HttpSession
对象。JWT的令牌过期时间能不能做成HttpSession
那样超时时间,只要用户间隔操作时间不超过15天,系统就不需要用户重新登录系统。实现这种效果的方案有两种:双Token
和Token缓存
,这里重点讲一下Token
缓存方案。
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
过期时间。
二、客户端如何更新令牌?
在我们的方案中,服务端刷新Token
过期时间,其实就是生成一个新的Token
给客户端。那么客户端怎么知道这次响应带回来的Token是更新过的呢?这个问题很容易解决。
只要用户成功登陆系统,当后端服务器更新Token
的时候,就在响应中添加Token
。客户端那边判断每次Ajax响应里面是否包含Token
,如果包含,就把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
类型的变量,可以用来保存线程安全的数据,而且避免了使用线程锁。
ThreadLocalToken
package com.example.emos.wx.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(); } }
注意事项:
因为在OAuth2Filter
类中要读写ThreadLocal
中的数据,所以OAuth2Filter
类必须要设置成多例的,否则ThreadLocal
将无法使用。
package com.example.emos.wx.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 { @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); } }
我们要创建的ShiroConfig
类,是用来把OAuth2Filter
和OAuth2Realm
配置到Shiro框架,这样我们辛苦搭建的Shiro+JWT才算生效。
package com.example.emos.wx.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);
// token保存在客户端,不保存在服务端 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<>();
// anon的过滤掉,其他的交给shiro处理 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(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
我们要创建AOP切面类
,拦截所有Web方法的返回值,在返回的R对象
中添加更新后的令牌。
package com.example.emos.wx.aop; import com.example.emos.wx.common.util.R; import com.example.emos.wx.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; @Aspect @Component public class TokenAspect { @Autowired private ThreadLocalToken threadLocalToken; @Pointcut("execution(public * com.example.emos.wx.controller.*.*(..)))") public void aspect() { } @Around("aspect()") public Object around(ProceedingJoinPoint point) throws Throwable { R r = (R) point.proceed(); //方法执行结果 String token = threadLocalToken.getToken(); //如果ThreadLocal中存在Token,说明是更新的Token if (token != null) { r.put("token", token); //往响应中放置Token threadLocalToken.clear(); } return r; } }
精简异常:
package com.example.emos.wx.config; import com.example.emos.wx.exception.EmosException; import lombok.extern.slf4j.Slf4j; 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 "后端执行异常"; } } }
标签:令牌,jwt,Token,token,org,import,shiro From: https://www.cnblogs.com/sgj191024/p/17744887.html