SpringBoot+shiro+jwt前后端分离鉴权

存在问题

分布式会话缓存是基于浏览器的Cookie机制,如果用户禁用Cookie,则无法实现功能。

解决方案

使用基于jwt的token生成方案:

  1. 用户登录之后,获得会话的SessionID,使用jwt根据SessionID颁发签名并设置过期时间(与Session过期时间相同)返回token
  2. 将token保存在客户端,并且每次发送请求时都在header上携带jwt的token
  3. shiroSessionManager继承DefaultWebSessionManager,重写 getSessionId方法。从header上检测是够携带token,如果携带,则解码token,使用jwttoken的jti作为sessionId

jwt概述

JWT(JSON WEB TOKEN):JSON网络令牌,JWT是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON格式)。它是在Web环境下两个实体之间传输数据的一项标准。实际上传输的就是一个字符串。

  • 广义上:JWT是一个标准的名称;

  • 狭义上:JWT指的就是用来传递的那个token字符串

JWT由三部分构成:header(头部)、payload(载荷)和signature(签名)。

  1. Header

    存储两个变量

    1. 秘钥(可以用来比对)
    2. 算法(也就是下面将Header和payload加密成Signature)
  2. payload

    存储很多东西,基础信息有如下几个

    1. 签发人,也就是这个"令牌"归属于哪个用户。一般是userId
    2. 创建时间,也就是这个令牌是什么时候创建的
    3. 失效时间,也就是这个令牌什么时候失效(session的失效时间)
    4. 唯一标识,一般可以使用算法生成一个唯一标识(jti==>sessionId)
  3. Signature

    这个是上面两个经过Header中的算法加密生成的,用于比对信息,防止篡改Header和payload

然后将这三个部分的信息经过加密生成一个JwtToken的字符串,发送给客户端,客户端保存在本地。当客户端发起请求的时候携带这个到服务端(可以是在cookie,可以是在header),在服务端进行验证,我们需要解密对于的payload的内容

集成jwt

编写JwtProperties配置类

java 复制代码
package com.itheima.shiro.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.io.Serializable;

/**
 * @Description:jw配置文件
 */
@Data
@ConfigurationProperties(prefix = "itheima.framework.jwt")
public class JwtProperties implements Serializable {

    /**
     * @Description 签名密码
     */
    private String base64EncodedSecretKey;
}

application.yml中jwt的相关配置

yaml 复制代码
itheima:
  resource:
    systemcode: shiro-mgt
  framework:
    jwt:
      base64-encoded-secret-key: qazwsx1234567890

编写JwtTokenManager类

java 复制代码
package com.itheima.shiro.core.impl;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.itheima.shiro.config.JwtProperties;
import com.itheima.shiro.utils.EmptyUtil;
import com.itheima.shiro.utils.EncodesUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Service;

import javax.xml.crypto.Data;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description:自定义jwtkoken管理者
 */
@Service("jwtTokenManager")
@EnableConfigurationProperties({JwtProperties.class})
public class JwtTokenManager {

    @Autowired
    JwtProperties jwtProperties;

    /**
     * @Description 签发令牌
     *      1、头部型
     *          密码签名
     *          加密算法
     *      2、payload
     *          签发的时间
     *          唯一标识
     *          签发者
     *          过期时间
     * @param iss 签发者
     * @param ttlMillis 过期时间
     * @param claims jwt存储的一些非隐私信息
     * @return
     */
    public String issuedToken(String iss, long ttlMillis, String sessionId, Map<String,Object> claims){
        if (EmptyUtil.isNullOrEmpty(claims)){
            claims = new HashMap<>();
        }
        //获取当前时间
        long nowMillis = System.currentTimeMillis();
        //获取加密签名
        String base64EncodedSecretKey = EncodesUtil.encodeBase64(jwtProperties.getBase64EncodedSecretKey().getBytes());
        //构建令牌
        JwtBuilder builder = Jwts.builder()
                .setClaims(claims)//构建非隐私信息
                .setId(sessionId)//构建唯一标识,此时使用shiro生成的唯一id
                .setIssuedAt(new Date(nowMillis))//构建签发时间
                .setSubject(iss)//签发者
                .signWith(SignatureAlgorithm.HS256, base64EncodedSecretKey);//指定算法和秘钥
        if (ttlMillis>0){
            long expMillis = nowMillis+ttlMillis;
            Date expData = new Date(expMillis);
            builder.setExpiration(expData);//指定过期时间
        }
        return builder.compact();
    }

    /**
     * @Description 解析令牌
     * @param jwtToken 令牌字符串
     * @return
     */
    public Claims decodeToken(String jwtToken){
        //获取加密签名
        String base64EncodedSecretKey = EncodesUtil.encodeBase64(jwtProperties.getBase64EncodedSecretKey().getBytes());
        //带着密码去解析字符串
        return Jwts.parser()
                .setSigningKey(base64EncodedSecretKey)
                .parseClaimsJws(jwtToken)
                .getBody();
    }


    /**
     * @Description 校验令牌:1、头部信息和荷载信息是否被篡改 2、校验令牌是否过期
     * @param  jwtToken 令牌字符串
     * @return
     */
    public boolean isVerifyToken(String jwtToken){
        //带着签名构建校验对象
        Algorithm algorithm = Algorithm.HMAC256(jwtProperties.getBase64EncodedSecretKey().getBytes());
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        //校验:如果校验1、头部信息和荷载信息是否被篡改 2、校验令牌是否过期 不通过则会抛出异常
        jwtVerifier.verify(jwtToken);
        return true;
    }

}

重写DefaultWebSessionManager

ShiroSessionManager主要是添加jwtToken的jti作为会话的唯一标识

java 复制代码
package com.itheima.shiro.core.impl;

import com.itheima.shiro.utils.EmptyUtil;
import io.jsonwebtoken.Claims;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 * @Description:自定义会话管理器
 */
public class ShiroSessionManager extends DefaultWebSessionManager {

    //从请求中获得sessionId的key
    private static final String AUTHORIZATION = "jwtToken";

    //自定义注入的资源类型名称
    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    @Autowired
    JwtTokenManager jwtTokenManager;

    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        //判断request请求中是否带有jwtToken的key
        String jwtToken = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        //如果没有走默认的cookie获得sessionId的方式
        if (EmptyUtil.isNullOrEmpty(jwtToken)){
            return super.getSessionId(request, response);
        }else {
               //如果有走jwtToken获得sessionI的的方式
            Claims claims = jwtTokenManager.decodeToken(jwtToken);
            String id = (String) claims.get("jti");
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                    REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        }

    }
}

重写默认过滤器

BaseResponse返回统一json的对象

java 复制代码
package com.itheima.shiro.core.base;

import com.itheima.shiro.utils.ToString;
import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * @Description 基础返回封装
 */
@Data
public class BaseResponse extends ToString {

    private Integer code ;

    private String msg ;

    private String date;

    private static final long serialVersionUID = -1;

    public BaseResponse(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public BaseResponse(Integer code, String msg, String date) {
        this.code = code;
        this.msg = msg;
        this.date = date;
    }
}

编写JwtAuthcFilter,校验访问是否合法和拒绝访问时返回信息的设置

使用wtTokenManager.isVerifyToken(jwtToken)校验颁发jwtToken是否合法,同时在拒绝的时候返回对应的json数据格式

java 复制代码
package com.itheima.shiro.core.filter;

import com.alibaba.fastjson.JSONObject;
import com.itheima.shiro.constant.ShiroConstant;
import com.itheima.shiro.core.base.BaseResponse;
import com.itheima.shiro.core.impl.JwtTokenManager;
import com.itheima.shiro.core.impl.ShiroSessionManager;
import com.itheima.shiro.utils.EmptyUtil;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

/**
 * @Description:自定义登录验证过滤器
 */
public class JwtAuthcFilter extends FormAuthenticationFilter {

    private JwtTokenManager jwtTokenManager;

    public JwtAuthcFilter(JwtTokenManager jwtTokenManager) {
        this.jwtTokenManager = jwtTokenManager;
    }

    /**
     * @Description 是否允许访问
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //判断当前请求头中是否带有jwtToken的字符串
        String jwtToken = WebUtils.toHttp(request).getHeader("jwtToken");
        //如果有:走jwt校验
        if (!EmptyUtil.isNullOrEmpty(jwtToken)){
            boolean verifyToken = jwtTokenManager.isVerifyToken(jwtToken);
            if (verifyToken){
                return super.isAccessAllowed(request, response, mappedValue);
            }else {
                return false;
            }
        }
        //没有没有:走原始校验
        return super.isAccessAllowed(request, response, mappedValue);
    }

    /**
     * @Description 访问拒绝时调用
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //判断当前请求头中是否带有jwtToken的字符串
        String jwtToken = WebUtils.toHttp(request).getHeader("jwtToken");
        //如果有:返回json的应答
        if (!EmptyUtil.isNullOrEmpty(jwtToken)){
            BaseResponse baseResponse = new BaseResponse(ShiroConstant.NO_LOGIN_CODE,ShiroConstant.NO_LOGIN_MESSAGE);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().write(JSONObject.toJSONString(baseResponse));
            return false;
        }
        //如果没有:走原始方式
        return super.onAccessDenied(request, response);
    }
}

编写 JwtPermsFilter

校验方式没变,只是改写了当访问拒绝时返回信息的方式

java 复制代码
package com.itheima.shiro.core.filter;

import com.alibaba.fastjson.JSONObject;
import com.itheima.shiro.constant.ShiroConstant;
import com.itheima.shiro.core.base.BaseResponse;
import com.itheima.shiro.utils.EmptyUtil;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

/**
 * @Description:自定义jwt的资源校验
 */
public class JwtPermsFilter extends PermissionsAuthorizationFilter {

    /**
     * @Description 访问拒绝时调用
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        //判断当前请求头中是否带有jwtToken的字符串
        String jwtToken = WebUtils.toHttp(request).getHeader("jwtToken");
        //如果有:返回json的应答
        if (!EmptyUtil.isNullOrEmpty(jwtToken)){
            BaseResponse baseResponse = new BaseResponse(ShiroConstant.NO_AUTH_CODE,ShiroConstant.NO_AUTH_MESSAGE);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().write(JSONObject.toJSONString(baseResponse));
            return false;
        }
        //如果没有:走原始方式
        return super.onAccessDenied(request, response);
    }
}

编写 JwtRolesFilter

java 复制代码
package com.itheima.shiro.core.filter;

import com.alibaba.fastjson.JSONObject;
import com.itheima.shiro.constant.ShiroConstant;
import com.itheima.shiro.core.base.BaseResponse;
import com.itheima.shiro.utils.EmptyUtil;
import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

/**
 * @Description:自定义jwt角色校验
 */
public class JwtRolesFilter extends RolesAuthorizationFilter {

    /**
     * @Description 访问拒绝时调用
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        //判断当前请求头中是否带有jwtToken的字符串
        String jwtToken = WebUtils.toHttp(request).getHeader("jwtToken");
        //如果有:返回json的应答
        if (!EmptyUtil.isNullOrEmpty(jwtToken)){
            BaseResponse baseResponse = new BaseResponse(ShiroConstant.NO_ROLE_CODE,ShiroConstant.NO_ROLE_MESSAGE);
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().write(JSONObject.toJSONString(baseResponse));
            return false;
        }
        //如果没有:走原始方式
        return super.onAccessDenied(request, response);
    }
}

在ShiroConfig中配置自定义的过滤器

java 复制代码
    @Autowired
    JwtTokenManager jwtTokenManager;

/**
 * @Description 自定义拦截器定义
 */
private Map<String, Filter> filters() {
    Map<String, Filter> map = new HashMap<String, Filter>();
    map.put("role-or", new RolesOrAuthorizationFilter());
    map.put("kicked-out", new KickedOutAuthorizationFilter(redissonClient(), redisSessionDao(), shiroSessionManager()));
    map.put("jwt-authc", new JwtAuthcFilter(jwtTokenManager));
    map.put("jwt-perms", new JwtPermsFilter());
    map.put("jwt-roles", new JwtRolesFilter());
    return map;
}

authentication.properties配置

properties 复制代码
/static/**=anon
#登录链接不拦截
/login/**=anon
#访问/resource/**需要有admin的角色
/resource/**=role-or[MangerRole,SuperAdmin]
#前后端分离校验角色
/role/** = kicked-out,jwt-roles[SuperAdmin]
#其他链接是需要登录的
/**=kicked-out,authc

业务代码编写

LoginAction改写

java 复制代码
/**
 * @Description jwt的json登录方式
 * @param loginVo
 * @return
 */
@RequestMapping("login-jwt")
@ResponseBody
public BaseResponse LoginForJwt(@RequestBody LoginVo loginVo){
    return loginService.routeForJwt(loginVo);
}

LoginService

java 复制代码
/**
 * @Description jwt方式登录
 @param loginVo 登录参数
  * @return
 */
public BaseResponse routeForJwt(LoginVo loginVo) throws UnknownAccountException,IncorrectCredentialsException;

LoginServiceImpl

java 复制代码
@Override
public BaseResponse routeForJwt(LoginVo loginVo) throws UnknownAccountException, IncorrectCredentialsException {
    //实现登录
    String jwtToken = null;
    try {
        SimpleToken simpleToken = new SimpleToken(null, loginVo.getLoginName(), loginVo.getPassWord());
        Subject subject = SecurityUtils.getSubject();
        subject.login(simpleToken);
        //登录完成之后需要颁发令牌
        String sessionId = ShiroUserUtil.getShiroSessionId();
        ShiroUser shiroUser = ShiroUserUtil.getShiroUser();
        Map<String,Object> claims = new HashMap<>();
        claims.put("shiroUser", JSONObject.toJSONString(shiroUser));
        jwtToken = jwtTokenManager.issuedToken("system", subject.getSession().getTimeout(), sessionId, claims);
        //缓存加载
        this.loadAuthorityToCache(simpleToken);
    }catch (Exception e){
        BaseResponse baseResponse = new BaseResponse(ShiroConstant.LOGIN_FAILURE_CODE, ShiroConstant.LOGIN_FAILURE_MESSAGE);
    }
    BaseResponse baseResponse = new BaseResponse(ShiroConstant.LOGIN_SUCCESS_CODE, ShiroConstant.LOGIN_SUCCESS_MESSAGE,jwtToken);
    return baseResponse;
}

LoginVo类(说明)

java 复制代码
package com.itheima.shiro.vo;

import com.itheima.shiro.utils.ToString;
import lombok.Getter;
import lombok.Setter;

/**
 * 
 * @ClassName: LoginVo
 *
 */
@Getter
@Setter
public class LoginVo extends ToString {
    
    /** serialVersionUID */
    private static final long serialVersionUID = 8393603366418306749L;

    /**登录名**/
    private String loginName;
    
    /**登录密码**/
    private String passWord;
    
    /**图片验证码**/
    private String radompicture;
    
    /**图片验证码key**/
    private String key;
    
    /**终端信息*/
    private String blackBox;
    
    /**终端类型(ios、android、h5)*/
    private String terminalType;
    
    /**登陆方式**/
    private String loginType;

    /**微信openId**/
    private String openId;
    
    /**系统**/
    private String systemCode;
    
    /**短信验证码**/
    private String checkCode;
    
    /**应用下载渠道code**/
    private String channelNo;
    
    /**推荐码**/
    private String recommendId;
    
    /**产品类型**/
    private String productType;


}
相关推荐
小当家.1052 小时前
JVM/八股详解(下部):垃圾收集、JVM 调优与类加载机制
java·jvm·面试
szm02252 小时前
Java并发
java·开发语言
天“码”行空2 小时前
java的设计模式-----------单例类
java·开发语言·设计模式
0***m8222 小时前
Java性能优化实战技术文章大纲性能优化的基本原则
java·开发语言·性能优化
芒克芒克2 小时前
JVM性能监控
java·jvm
行稳方能走远2 小时前
Android java 学习笔记3
android·java
WF_YL2 小时前
IntelliJ IDEA 关闭保存时在文件末尾换行 -(取消保存自动末尾换行)
java·ide·intellij-idea
撩得Android一次心动2 小时前
Android Lifecycle 全面解析:掌握生命周期管理的艺术(1)
android·java·kotlin·lifecycle