1.Shiro用来认证用户及权限控制,jwt用来生成一个token令牌,暂存用户信息。令牌存储在客户端,用户每次请求将其放在header中,在每个服务器节点进行验证。
2.导入依赖库:
- <dependency>
- <groupId>org.apache.shirogroupId>
- <artifactId>shiro-webartifactId>
- <version>1.5.3version>
- dependency>
- <dependency>
- <groupId>org.apache.shirogroupId>
- <artifactId>shiro-springartifactId>
- <version>1.5.3version>
- dependency>
- <dependency>
- <groupId>com.auth0groupId>
- <artifactId>java-jwtartifactId>
- <version>3.10.3version>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-configuration-processorartifactId>
- <optional>trueoptional>
- dependency>
- <dependency>
- <groupId>org.apache.commonsgroupId>
- <artifactId>commons-lang3artifactId>
- <version>3.11version>
- dependency>
- <dependency>
- <groupId>org.apache.httpcomponentsgroupId>
- <artifactId>httpcoreartifactId>
- <version>4.4.13version>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-aopartifactId>
- dependency>
3.定义密钥和过期时间
这个过程一般在springboot配置文件中进行编辑,然后再注入到Javabean中,维护起来方便。
- emos:
- jwt:
- #密钥
- secret: abc123456
- #令牌过期时间(天)
- expire: 5
- #令牌缓存时间(天数)
- cache-expire: 10
4.创建JWT工具类
可以创建一个shiro包,然后再创建类,该类主要方法包括创建token,获得token用户id,验证token
- 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);
- }
-
- }
-
5.把令牌封装成认证对象
因为Shiro框架认证需要用到认证对象,把令牌字符串做简单的封装。客户端提交的token不能直接交给Shiro框架,需要向封装成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;
- }
- }
-
6.创建OAuth2Realm类,实现AuthorizingRealm接口
- 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;
- }
- }
7.刷新令牌如何设计?
一个是采用短令牌和长令牌的双令牌机制;一个是采用redis缓存令牌机制。更常用第二种;



8.创建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();
- }
- }
-
9.创建OAuth2Filter类

- emos:
- jwt:
- #密钥
- secret: abc123456
- #令牌过期时间(天)
- expire: 5
- #令牌缓存时间(天数)
- cache-expire: 10
- 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);
- }
- }
-
10.创建ShiroConfig类

- 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);
- securityManager.setRememberMeManager(null);
- return securityManager;
- }
-
- @Bean("shiroFilter")
- public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,OAuth2Filter oAuth2Filter) {
- ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
- shiroFilter.setSecurityManager(securityManager);
-
- //oauth过滤
- Map
filters = new HashMap<>(); - filters.put("oauth2", oAuth2Filter);
- shiroFilter.setFilters(filters);
-
- Map
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();
- }
-
- @Bean
- public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
- AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
- advisor.setSecurityManager(securityManager);
- return advisor;
- }
-
- }
-
-
-
11.利用aop,把更新的令牌返回给客户端

- 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;
- }
- }
12.返回给客户端的异常精简化,进行测试
- 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 "后端执行异常";
- }
-
- }
-
- }
-