Shiro整合JWT

1、概述

ServletSession机制流程如下

  • 用户首次发请求
  • 服务器接收到请求之后,无论你有没有权限访问到资源,在返回响应的时候,服务器都会生成一个Session用来储存该用户的信息,然后生成SessionId作为对应的Key
  • 服务器会在响应中,用SessionId这个名字,把这个SessionIdCookie的方式发给客户(就是Set-Cookie响应头)
  • 由于已经设置了Cookie,下次访问的时候,服务器会自动识别到这个SessionId然后找到你上次对应的Session

引入Shiro后,新的流程如下

  • 用户首次发请求。
  • 服务器接收到请求之后,无论你有没有权限访问到资源,在返回响应的时候,服务器都会生成一个Session用来储存该用户的信息,然后生成SessionId作为对应的Key,还会创建一个Subject对象(就是Shiro中用来代表当前用户的类),也用这个SessionId作为Key绑定。
  • 服务器会在响应中,用SessionId这个名字,把这个SessionIdCookie的方式发给客户(就是Set-Cookie响应头)。
  • 第二次接受到请求的时候,Shiro会从请求头中找到SessionId,然后去寻找对应的Subject然后绑定到当前上下文,这时候Shiro就能知道来访的是谁了。

对于以上流程,都和浏览器中的Cookie密切相关的,对于一个前后端分离的系统而言,一般是需要支持多端的,一个api要支持H5, PCAPP三个前端,如果使用session的话对app不是很友好,而且session有跨域攻击的问题

2、整合流程

Shiro集成JWT需要禁用session,禁用后服务器将不会再维护用户的状态,达到无状态调用的目的

/**
 * @param realm
 * @return DefaultWebSecurityManager
 * @description 注入安全管理器
 * @author PengHuAnZhi
 * @date 2021/11/4 20:50
 */
@Bean("defaultWebSecurityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(CustomRealm realm) {
    DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
    //开启全局缓存
    realm.setCachingEnabled(true);
    //开启认证缓存
    realm.setAuthenticationCachingEnabled(true);
    realm.setAuthenticationCacheName("authenticationCache");
    //开启授权缓存
    realm.setAuthorizationCachingEnabled(true);
    realm.setAuthorizationCacheName("authorizationCache");
    realm.setCacheManager(new EhCacheManager());
    defaultSecurityManager.setRealm(realm);
    //关闭shiro自带的session
    DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
    DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
    defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
    subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
    defaultSecurityManager.setSubjectDAO(subjectDAO);
    return defaultSecurityManager;
}
/**
 * @author PengHuAnZhi
 * @ProjectName ShiroSpringBootDemo
 * @Description TODO
 * @time 2021/11/13 16:25
 */
public class NoSessionWebSubjectFactory extends DefaultWebSubjectFactory {
    @Override
    public Subject createSubject(SubjectContext context) {
        // 禁用session
        context.setSessionCreationEnabled(false);
        return super.createSubject(context);
    }
}

然后定义一个JwtToken,用于封装UserNameToken,并且要使其充当Shiro中的令牌,所以需要实现AuthenticationToken接口,重写里面的获取用户信息getPrincipal和获取凭证信息的getCredentials两个方法

/**
 * @author PengHuAnZhi
 * @ProjectName ShiroSpringBootDemo
 * @Description TODO
 * @time 2021/11/13 15:36
 */
public class JwtToken implements AuthenticationToken {
    private final String token;

    public JwtToken(String token) {
        this.token = token;
    }

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

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

定义Jwt相关的工具类

/**
 * @author PengHuAnZhi
 * @ProjectName ShiroSpringBootDemo
 * @Description TODO
 * @time 2021/11/13 15:49
 */
public class JwtUtil {
    /**
     * JWT-account
     */
    public static final String ACCOUNT = "userName";
    /**
     * JWT-currentTimeMillis
     */
    public final static String CURRENT_TIME_MILLIS = "currentTimeMillis";
    /**
     * 有效期时间2小时
     */
    public static final long EXPIRE_TIME = 2 * 60 * 60 * 1000L;
    /**
     * 秘钥
     */
    public static final String SECRET_KEY = "shiroKey";


    /**
     * 生成签名返回token
     *
     * @param userName          用户名
     * @param currentTimeMillis 当前时间戳
     * @return 返回一个token
     */
    public static String sign(String userName, String currentTimeMillis) {
        // 帐号加JWT私钥加密
        String secret = userName + SECRET_KEY;
        // 此处过期时间,单位:毫秒,在当前时间到后边的20分钟内都是有效的
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        //采用HMAC256加密
        Algorithm algorithm = Algorithm.HMAC256(secret);
        return JWT.create()
                .withClaim(ACCOUNT, userName)
                .withClaim(CURRENT_TIME_MILLIS, currentTimeMillis)
                .withExpiresAt(date)
                //创建一个新的JWT,并使用给定的算法进行标记
                .sign(algorithm);
    }

    /**
     * 校验token是否正确
     *
     * @param token token
     * @return 是否登录成功
     */
    public static boolean verify(String token) {
        String secret = getClaim(token, ACCOUNT) + SECRET_KEY;
        Algorithm algorithm = Algorithm.HMAC256(secret);
        JWTVerifier verifier = JWT.require(algorithm)
                .build();
        verifier.verify(token);
        return true;
    }

    /**
     * 获得Token中的信息无需secret解密也能获得
     *
     * @param token token
     * @param claim token中的信息
     * @return 返回解密后的信息
     */
    public static String getClaim(String token, String claim) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(claim).asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
}

然后再定义一个JwtFilter用于过滤所有请求,继承BasicHttpAuthenticationFilter,将其交给Shiro验证

/**
 * @author PengHuAnZhi
 * @ProjectName ShiroSpringBootDemo
 * @Description TODO
 * @time 2021/11/13 15:39
 */
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter implements Filter {
    /**
     * 设置请求头中需要传递的字段名
     */
    protected static final String AUTHORIZATION_HEADER = "Access-Token";

    /**
     * @param request     请求
     * @param response    响应
     * @param mappedValue 就是[urls]配置中拦截器参数部分
     * @return boolean 表示是否允许访问,如果允许访问返回true,否则false
     * @date 2020/11/24
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return false;
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        JwtToken token = new JwtToken(((HttpServletRequest) request).getHeader(AUTHORIZATION_HEADER));
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * @param request  请求
     * @param response 响应
     * @return boolean 表示当访问拒绝时是否已经处理了,如果返回true表示需要继续处理,如果返回false表示该拦截器实例已经处理了,将直接返回即可
     * @date 2020/11/24
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        String jsonWebToken = ((HttpServletRequest) request).getHeader(AUTHORIZATION_HEADER);
        String username = "";
        if (StringUtils.isBlank(jsonWebToken)) {
            jsonWebToken = "";
        } else {
            // 解码 jwt
            DecodedJWT decodeJwt = JWT.decode(jsonWebToken);
            username = decodeJwt.getClaim("userName").asString();
            System.out.println(username + "登录");
        }
        JwtToken token = new JwtToken(jsonWebToken);
        try {
            // 交给自定义realm进行jwt验证和对应角色,权限的查询
            getSubject(request, response).login(token);
        } catch (AuthenticationException e) {
            request.setAttribute("msg", "认证失败");
            // 转发给指定的 controller, 进行统一异常处理
            request.getRequestDispatcher("/exception").forward(request, response);
            return false;
        }
        return true;
    }
}

在自定义Realm中验证jwtrolepermission

/**
 * @author PengHuAnZhi
 * @ProjectName ShiroSpringBootDemo
 * @Description TODO
 * @time 2021/11/4 20:44
 */
@Component
public class CustomRealm extends AuthorizingRealm {
    @Resource
    UserServiceImpl userServiceImpl;

    /**
     * @param token jwt
     * @return boolean
     * @description 验证是不是自己的token类型
     * @author PengHuAnZhi
     * @date 2021/11/13 16:06
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 获取用户名, 用户唯一标识
        String username = JwtUtil.getClaim(principals.toString(), "userName");
        User user = userServiceImpl.findByUserName(username);
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        if (user == null) {
            return null;
        }
        authorizationInfo.addRole("admin");
        authorizationInfo.addStringPermission("user:update:*");
        authorizationInfo.addStringPermission("product:*:*");
        return authorizationInfo;
    }

    @SneakyThrows
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String credentials = (String) token.getCredentials();
        String userName;
        try {
            //jwt验证token
            boolean verify = JwtUtil.verify(credentials);
            if (!verify) {
                throw new AuthenticationException("Token校验不正确");
            }
            userName = JwtUtil.getClaim(credentials, JwtUtil.ACCOUNT);
        } catch (Exception e) {
            throw new Exception("用户身份校验失败");
        }

        //交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,不设置则使用默认的SimpleCredentialsMatcher
        return new SimpleAuthenticationInfo(
                //用户名
                userName,
                //凭证
                credentials,
                //realm name
                this.getName());
    }
}

定义一个登录接口

@RequestMapping("/login")
public String login(String userName, String password, HttpServletResponse response) {
    try {
        if (!"phz".equals(userName) || !"123".equals(password)) {
            System.out.println("用户名错误");
            return "redirect:/login.jsp";
        }
        //生成token
        String token = JwtUtil.sign(userName, System.currentTimeMillis());
        //写入header
        response.setHeader("Access-Token", token);
        response.setHeader("Access-Control-Expose-Headers", "Access-Token");
    } catch (IncorrectCredentialsException e) {
        e.printStackTrace();
        System.out.println("密码错误");
        return "redirect:/login.jsp";
    } catch (UnknownAccountException e) {
        e.printStackTrace();
        System.out.println("用户名错误");
        return "redirect:/login.jsp";
    }
    return "redirect:/index.jsp";
}

最后将自己定义的Filter添加到Shiro

/**
 * @return ShiroFilterFactoryBean
 * @description 1、创建ShiroFilter,拦截所有请求
 * @author PengHuAnZhi
 * @date 2021/11/4 20:43
 */
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    //给filter设置安全管理器
    shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
    //添加自定义过滤器
    Map<String, Filter> filterMap = new HashMap<>(1);
    filterMap.put("jwt", new JwtFilter());
    shiroFilterFactoryBean.setFilters(filterMap);
    //配置系统受限资源和公共资源
    Map<String, String> map = new HashMap<>();
    map.put("/register.jsp", ANON);
    map.put("/login.jsp", ANON);
    map.put("/user/login", ANON);
    map.put("/user/register", ANON);
    //使用自己的filter对所有请求拦截
    map.put("/**", "jwt");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
    //不设置默认也是login.jsp
    shiroFilterFactoryBean.setLoginUrl("/login.jsp");
    return shiroFilterFactoryBean;
}
上一篇:2、Web项目,Spring整合Shiro


下一篇:Shiro简介及SpringBoot集成Shiro(狂神说视频简易版)