1.新建一个springboot项目

2.引入相关maven依赖

<!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.13</version>
        </dependency>
        <!--swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--validation-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.4.0</version>
        </dependency>
        <!--shiro、JWT-->
        <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.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>

3.yml配置

server:
  tomcat:
    uri-encoding: UTF-8
    threads:
      max: 200
      min-spare: 30
    connection-timeout: 5000ms
  port: 8080

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/springboot_shiro_jwt?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: mima
      initial-size: 8 #连接池初始连接数
      max-active: 16 #连接池最大连接数
      min-idle: 8 #连接池最小连接数
      max-wait: 60000 #最大等待时间  单位:毫秒
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false

  #redis配置
  redis:
    database: 0
    host: localhost
    port: 6379
    password:
    jedis:
      pool:
        max-active: 1000 #redis连接池连接数量上限
        max-wait: -1ms
        max-idle: 16 #最大空闲连接数
        min-idle: 8 #最小空闲连接数

mybatis:
  mapper-locations: classpath*:mapper/*.xml
  type-aliases-package: com.hds.shiro_jwt_swagger2.pojo
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true


emos:
  jwt:
    secret: abc123456 #密钥
    expire: 5 #令牌过期时间
    cache-expire: 10 #令牌缓存时间

4.自定义异常

①定义统一返回对象R

public class R extends HashMap<String, Object> {

    public R() {
        put("code", HttpStatus.SC_OK);
        put("msg", "success");
    }

    public R put(String key, Object value) {
        super.put(key, value);
        return this;
    }

    public static R ok() {
        return new R();
    }

    public static R ok(String msg) {
        R r = new R();
        r.put("msg", msg);
        return r;
    }

    public static R ok(Map<String, Object> map) {
        R r = new R();
        r.putAll(map);
        return r;
    }

    public static R error(int code, String msg) {
        R r = new R();
        r.put("code", code);
        r.put("msg", msg);
        return r;
    }

    public static R error(String msg) {
        return R.error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
    }

    public static R error() {
        return R.error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员!");
    }
}

②定义ResponseEnum

@Getter
public enum ResponseEnum {

    ERROR(-1, "服务端错误"),

    NOHANDLERFOUND(404, "404"),

    SUCCESS(0, "成功"),

    PASSWORD_ERROR(1, "密码错误"),

    USERNAME_EXIST(2, "用户名已存在"),

    PARAM_ERROR(3, "参数错误"),

    EMAIL_EXIST(4, "邮箱已存在"),

    CODE_OUT(8, "验证码过期"),

    CODE_ERROR(9, "验证码错误"),

    NEED_LOGIN(10, "用户未登录, 请先登录"),

    USERNAME_OR_PASSWORD_ERROR(11, "用户名或密码错误"),

    NO_PERMISSION(12, "你不具备相关权限"),

    ;

    Integer code;

    String desc;

    ResponseEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

③定义MyException继承自RuntimeException

/**
 * RunTimeException:运行时异常,又称不受检查异常,不受检查!
 * 因为不受检查,所以在代码中可能会有RunTimeException时Java编译检查时不会告诉你有这个异常,但是在实际运行代码时则会暴露出来,比如经典的1/0,空指针等。如果不处理也会被Java自己处理。
 */
@Data
public class MyException extends RuntimeException {

    private int code = 500;
    private String msg;

    public MyException(String msg) {
        super(msg);
        this.msg = msg;
    }

    public MyException(String msg, int code) {
        super(msg);
        this.msg = msg;
        this.code = code;
    }

}

④定义ExceptionAdvice捕获全局异常

@RestControllerAdvice
public class ExceptionAdvice {

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)/*出现异常,返回的状态码500*/
    @ExceptionHandler(Exception.class)
    public R exceptionHandler(Exception e){
        if(e instanceof MethodArgumentNotValidException){/*参数异常*/
            MethodArgumentNotValidException exception= (MethodArgumentNotValidException) e;
            return R.error(ResponseEnum.PARAM_ERROR.getCode(),exception.getBindingResult().getFieldError().getDefaultMessage());
        }
        else if(e instanceof MyException){
            MyException exception= (MyException) e;
            return R.error(exception.getCode(),exception.getMsg());
        }
        else if(e instanceof UnauthorizedException){
            return R.error(ResponseEnum.NO_PERMISSION.getCode(),ResponseEnum.NO_PERMISSION.getDesc());
        }
        else{
            return R.error(ResponseEnum.ERROR.getCode(),ResponseEnum.ERROR.getDesc());
        }
    }
}

5.集成swagger

创建SwaggerConfig对象

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket createRestApi() {
        Docket docket = new Docket(DocumentationType.SWAGGER_2);//指定api类型为swagger2
        ApiInfoBuilder builder = new ApiInfoBuilder();
        builder.title("shiro_jwt_swagger2"); // 文档页标题
        ApiInfo info = builder.build();
        docket.apiInfo(info);

        ApiSelectorBuilder selectorBuilder = docket.select();
        selectorBuilder.paths(PathSelectors.any());
        selectorBuilder.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class));
        docket = selectorBuilder.build();

        //全站统一header设置 请求头的token
        ApiKey apiKey = new ApiKey("token", "token", "header");
        List<ApiKey> apiKeyList = new ArrayList<>();
        apiKeyList.add(apiKey);
        docket.securitySchemes(apiKeyList);

        //全局Authorization参数
        AuthorizationScope scope = new AuthorizationScope("global", "accessEverything");
        AuthorizationScope[] scopes = {scope};
        SecurityReference reference = new SecurityReference("token", scopes);
        List refList = new ArrayList();
        refList.add(reference);
        SecurityContext context = SecurityContext.builder().securityReferences(refList).build();
        List cxtList = new ArrayList();
        cxtList.add(context);
        docket.securityContexts(cxtList);

        return docket;
    }
}

6.配置后端验证

生成实体类,dao层,idea配置database,通过mybatis-generator插件自动生成代码

springboot oauth2 jwt token 解析扩展信息_连接数

springboot oauth2 jwt token 解析扩展信息_List_02

 

springboot oauth2 jwt token 解析扩展信息_spring_03

 

springboot oauth2 jwt token 解析扩展信息_List_04

springboot oauth2 jwt token 解析扩展信息_spring_05

 

 

springboot oauth2 jwt token 解析扩展信息_List_06

 

 7.整合jwt+shiro

shiro:认证(登录核验身份)+授权(划分用户行为)框架。

shiro是利用过滤器(Filter),对每个Http请求进项过滤从而完成认证与授权的

①创建JwtUtil类,用来生成token以及验证token

/**
 * JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:
 * xxxxx.yyyyy.zzzzz
 * Header
 * Payload
 * Signature
 */
@Component
public class JwtUtil {

    @Value("${conf.jwt.secret}")
    private String secret;

    @Value("${conf.jwt.expire}")
    private int expire;


    /**
     * 创建token
     *
     * @param userId
     * @return
     */
    public String createToken(int userId) {
        Date date = DateUtil.offset(new Date(), DateField.DAY_OF_YEAR, 5);
        Algorithm algorithm = Algorithm.HMAC256(secret);
        JWTCreator.Builder builder = JWT.create();
        String token = builder.withClaim("userId", userId).withExpiresAt(date).sign(algorithm);
        return token;
    }

    /**
     * 通过token获取userid
     *
     * @param token
     * @return
     */
    public int getUserId(String token) {
        DecodedJWT jwt = JWT.decode(token);
        int userId = jwt.getClaim("userId").asInt();
        return userId;
    }

    /**
     * 验证token
     *
     * @param token
     */
    public void verifierToken(String token) {
        Algorithm algorithm = Algorithm.HMAC256(secret);
        JWTVerifier verifier = JWT.require(algorithm).build();
        verifier.verify(token);
    }
}

 ②创建OAuth2Token类

客户端提交的token字符串不能直接交给shiro框架,需要先封装成AuthenticationToken对象交给shiro。

/**
 * 认证对象
 * 把token字符串封装成shiro所需要的认证对象
 */
public class OAuth2Token implements AuthenticationToken {

    private String token;

    private OAuth2Token(String token){
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

③创建OAuth2Realm类

OAuth2Realm类是AuthorizingRealm的实现类,主要完成认证和授权的功能。

/**
 * Realm类  定义shiro认证和授权的方法
 */
@Component
public class OAuth2Realm extends AuthorizingRealm {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private UserService userService;

    /**
     *
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof OAuth2Token;
    }

    /**
     * 授权(验证权限时调用)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection collection) {
        //得到用户信息
        TbUser user= (TbUser) collection.getPrimaryPrincipal();
        int userId=user.getId();
        Set<String> permsSet=userService.searchUserPermissions(userId);
        SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();//授权对象
        //查询用户的权限列表
        //把权限列表添加到Info对象中
        info.setStringPermissions(permsSet);
        return info;//返回授权对象
    }

    /**
     * 认证(验证是否登录时调用)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //从令牌获取userId,然后检测该账户是否被冻结
        String accessToken=(String)token.getPrincipal();
        int userId=jwtUtil.getUserId(accessToken);
        TbUser user=userService.searchById(userId);
        if(user==null){
            throw new LockedAccountException("账号已被锁定,请联系管理员");
        }
        //向info对象添加用户信息、token字符串
        //SimpleAuthenticationInfo(用户信息,令牌字符串,Realm的名字)
        SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(user,accessToken,getName());
        return info;
    }
}

8.设计令牌续期机制

缓存令牌机制,将令牌缓存在Redis上面,缓存的令牌过期时间是客户端令牌过期时间的一倍,如果客户端令牌过期,缓存令牌没有过期,就生成新的令牌返回给客户端,并且服务端缓存新的令牌。如果客户端令牌过期,缓存令牌也过期了,则用户必须重新登录。客户端需要检测响应里面是否含有token,如果有token,就覆盖token。

如何在响应添加令牌?

springboot oauth2 jwt token 解析扩展信息_List_07

 定义OAuth2Filter类拦截所有HTTP请求,一方面它会把请求中的Token字符串提取出来,封装成对象交给shiro框架;另一方面,它会检查Token的有效性。如果Token过期,就会生成新的Token,分别存储在ThreadLocalToken和Redis中。

之所以要把新令牌保存到ThreadLocalToken里面,是因为要向AOP切面类传递这个新令牌。AOP切面类判断ThreadLocalToken里面是否有值,有值就会向返回R对象添加token,然后客户端需要检测响应里面是否含有token,如果有token,就覆盖token。

①创建ThreadLocalToken类

@Component
public class ThreadLocalToken {
    private ThreadLocal<String> local=new ThreadLocal<>();

    public void setToken(String token){
        local.set(token);
    }

    public String getToken(){
        return local.get();
    }

    public void clear(){
        local.remove();
    }
}

②创建OAuth2Filter类

因为OAuth2Filter类中要读写ThreadLocal中的数据,所以OAuth2Filter类必须设置成多例的!

@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;

    /**
     * 拦截请求之后,用于把令牌字符串封装成令牌对象
     * 该令牌对象会交给Shiro使用
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req= (HttpServletRequest) request;
        String token=getRequestToken(req);
        if(StrUtil.isBlank(token)){
            return null;
        }
        return new OAuth2Token(token);
    }


    /**
     * 拦截请求判断是否需要被shiro处理
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest req= (HttpServletRequest) request;
        //ajax提交application/json请求的时候,会先发出option请求
        //这里要放行option请求,不需要shiro处理
        if(req.getMethod().equals(RequestMethod.OPTIONS.name())){
            //放行
            return true;
        }
        //除了option请求之外,所有请求都要被Shiro处理
        return false;
    }

    /**
     * isAccessAllowed执行后,判断需要被shiro处理的才会执行该方法
     * 判断token是真过期还是假过期
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req= (HttpServletRequest) request;
        HttpServletResponse resp= (HttpServletResponse) response;
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");
        //允许跨域
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        //清空threadLocal
        threadLocalToken.clear();
        String token=getRequestToken(req);
        if(StrUtil.isBlank(token)){
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("无效的令牌");
            return false;
        }
        try{
            //验证token是否有效,如果验证有问题(1.令牌内容有问题、2.令牌过期)就会抛出异常
            jwtUtil.verifierToken(token);
        }catch (TokenExpiredException e){//令牌过期异常
            //判断redis是否还有令牌
            if(redisTemplate.hasKey(token)){
                //redis里面存在令牌,说明客户端的令牌过期,但是redis令牌还未过期
                //删除redis的 旧令牌
                //获取userid并生成新的令牌,并存进redis和threadLocalToken
                redisTemplate.delete(token);
                int userId=jwtUtil.getUserId(token);
                token=jwtUtil.createToken(userId);
                redisTemplate.opsForValue().set(token,userId+"",cacheExpire, TimeUnit.DAYS);
                threadLocalToken.setToken(token);
            }
            else{//客户端令牌过期了,并且服务端的令牌也过期了,此时需要重新登陆
                resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
                resp.getWriter().print("令牌已过期");
                return false;
            }
        }catch (Exception e){
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("无效的令牌");
            return false;
        }
        //验证通过
        //执行Realm类
        boolean bool=executeLogin(request,response);
        return bool;
    }

    /**
     * shiro执行Realm类里面的认证(只有认证失败才进来)方法后,判断用户未登录或者登录失败,则会执行该方法。
     * 该方法往客户端返回一个错误消息即可
     * @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.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
        try{
            resp.getWriter().print(e.getMessage());
        }catch (Exception exception){

        }
        return false;
    }

    /**
     * 管理拦截和响应的Filter
     * @param request
     * @param response
     * @param chain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest req= (HttpServletRequest) request;
        HttpServletResponse resp= (HttpServletResponse) response;
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        super.doFilterInternal(request, response, chain);

    }

    /**
     * 从请求中获取token
     * @param request
     * @return
     */
    private String getRequestToken(HttpServletRequest request){
        //从请求头获取token
        String token=request.getHeader("token");
        if(StrUtil.isBlank(token)){
            //从请求体获取token
            token=request.getParameter("token");
        }
        return token;
    }
}

③创建ShiroConfig类,把Filter和Realm添加到Shiro框架

@Configuration
public class ShiroConfig {

    /**
     * 用于封装Realm对象
     *
     * @param realm
     * @return
     */
    @Bean("securityManager")
    public SecurityManager securityManager(OAuth2Realm 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, OAuth2Filter filter) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        //oauth过滤
        Map<String, Filter> map = new HashMap<>();
        map.put("oauth2", filter);
        shiroFilter.setFilters(map);

        //设置拦截的请求
        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("/meeting/recieveNotify", "anon");
        //除了以上请求,其余的全部拦截
        filterMap.put("/**", "oauth2");//对应上面的map的oauth2
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }


    /**
     * 管理shiro对象生命周期
     * @return
     */
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }


    /**
     * AOP切面类   Web方法执行前,验证权限
     * 用于调用前判断 必须满足某角色某权限
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

④利用aop切面向客户端返回新的令牌

拦截web方法返回的R对象,然后向R对象添加Token。

创建TokenAspect类

@Aspect
@Component
public class TokenAspect {
    @Autowired
    private ThreadLocalToken threadLocalToken;

    /**
     * 切点,需要被拦截的点
     */
    @Pointcut("execution(public * com.hds.shiro_jwt_swagger2.controller.*.*(..))")
    public void aspect() {

    }

    /**
     * 针对切点定义环绕事件
     * 获得拦截方法换回的R对象,向R对象put token
     *
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("aspect()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        R r = (R) point.proceed();
        String token = threadLocalToken.getToken();
        if (token != null) {
            r.put("token", token);
            threadLocalToken.clear();
        }
        return r;
    }
}