springboot+shiro+jwt实现登录+权限验证

目录

一、简介:

JWT优点:

JWT缺点:

shiro:

JWT:

1.JWT头

2.有效载荷

3.签名哈希

4.Base64URL算法

二、实现

1.引入maven依赖

2.编写shiro配置类

3.定义token实体继承shiro的token

4.编写token处理工具类

5.自定义shiro的realm

6.JWTfilter过滤器,处理权限验证等

7.登出过滤器


一、简介:

JWT优点:

1.基于Token的身份认证是无状态的,服务器或者Session中不会存储任何用户信息-应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。

2.支持跨域访问: Cookie是不允许垮域访问的,token支持。

3.解耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在 你的API被调用的时候, 你可以进行Token生成调用即可。

4.更适用于移动应用: Cookie不支持手机端访问,token支持。

5.性能: token生成后其实就是一个字符串,在网络传输的过程中,性能更好。

6.基于标准化: 你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在 多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如: Firebase,Google, Microsoft)。

JWT缺点:

1.占带宽

正常情况下要比 session_id 更大,需要消耗更多流量,挤占更多带宽,假如你的网站每月有 10 万次的浏览器,就意味着要多开销几十兆的流量。实际上,许多人会在 JWT 中存储的信息会更多。

2.无法在服务端注销,那么久很难解决劫持问题

3.性能问题

JWT 的卖点之一就是加密签名,由于这个特性,接收方得以验证 JWT 是否有效且被信任。但是大多数 Web 身份认证应用中,JWT 都会被存储到 Cookie 中,这就是说你有了两个层面的签名。为此,你需要花费两倍的 CPU 开销来验证签名。对于有着严格性能要求的 Web 应用,这并不理想,尤其对于单线程环境。

shiro:

Shiro 是 Java 的一个安全框架。目前Shiro 的使用者有很多,因为它简单、功能够用;它虽然没有 Spring Security 的功能强大,但是在实际工作确实也用不到太多强大的功能,使用小而简单的 Shiro 就够了。Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。Shiro 可以帮助我们完成:加密、会话管理、认证、授权、与 Web 集成、缓存等。本文主要使用其认证与授权功能;shiro的身份认证:

即在应用中谁能证明他就是他本人。一般提供如他们的身份 ID 一些标识信息来表明他就是他本人,如提供身份证,用户名 / 密码来证明。在 shiro 中,用户需要提供 principals (身份)和 credentials(证明)给 shiro,从而应用能验证用户身份:

principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号。

credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。

最常见的 principals 和 credentials 组合就是用户名 / 密码了。

另外两个相关的概念是 Subject 及 Realm,分别是主体及验证主体的数据源。

Shiro 的 API 也是非常简单;其基本功能点如下图所示:

JWT:

JWT使用方式:服务器登录成功后创建一个令牌返回给客户端,客户端保存令牌,而服务器不保存令牌,每个请求令牌都被发送回服务器,服务器再对令牌进行处理校验。

JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSAECDSA的公钥/私钥对对JWT进行签名

JWT对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。注意JWT对象为一个长字串,各字串之间也没有换行符,此处为了演示需要,我们特意分行并用不同颜色表示了。每一个子串表示了一个功能块,总共有以下三个部分:JWT头、有效载荷和签名,将它们写成一行如下:

springboot+shiro+jwt实现登录+权限验证_第1张图片

1.JWT头

JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示。

{

"alg": "HS256",

"typ": "JWT"

}

在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。

最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。

2.有效载荷

有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择。

iss:发行人

exp:到期时间

sub:主题

aud:用户

nbf:在此之前不可用

iat:发布时间

jti:JWT ID用于标识该JWT

除以上默认字段外,我们还可以自定义私有字段,如下例:

{

"sex": "男",

"name": "chongchong",

"age": 23

}

默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。

JSON对象也使用Base64 URL算法转换为字符串保存。

3.签名哈希

签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)。在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象。

4.Base64URL算法

如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+","/"和"=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_"替换。

二、实现

其实太底层的原理,作者也没深入研究,所以不多说废话了,进入搬砖模式吧,本文中贴出代码中有不存在的类,在文末会提供项目地址,可下载项目查看:

1.引入maven依赖


        
        
            org.apache.shiro
            shiro-spring
            1.4.0
        
        
        
            com.auth0
            java-jwt
            3.4.0
        

2.编写shiro配置类

import com.liu.filter.LicenseFilter;
import com.liu.filter.URLPathMatchingFilter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
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.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

/**
 * Shiro配置类
 *
 * @author kevin
 * @date 2019/5/18
 */
@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置过滤器
        shiroFilterFactoryBean.setFilters(filters());

        //设置安全权限认证管理器
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/");
        shiroFilterFactoryBean.setUnauthorizedUrl(null);

        // 配置映射关系,authc:对应的所有url都必须认证通过才可以访问; anon:对应的所有url都都可以匿名访问
        // 必须是LinkedHashMap,因为要保证有序
        Map filterChainDefinitionMap = new LinkedHashMap<>();
        //swagger的一些资源不拦截
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/swagger**/**", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");

        //静态资源不被拦截
        filterChainDefinitionMap.put("**.js", "anon");
        filterChainDefinitionMap.put("/static/**", "anon");
        //系统中部分地址不经过过滤器
        filterChainDefinitionMap.put("/user/checkToken","anon");
        filterChainDefinitionMap.put("/file/**","anon");
        filterChainDefinitionMap.put("/user/**","anon");
        //配置登录、退出,登录自己实现,登出自定义filter实现(为了实现自己的业务)
        filterChainDefinitionMap.put("/user/login","anon");
        filterChainDefinitionMap.put("/user/logout","logout");

        //加入自定义的过滤器,配置软件授权、JWT与URL的权限过滤器
        filterChainDefinitionMap.put("/**", "jwt");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

    private Map filters(){
        Map filters = new LinkedHashMap<>(4);
        //JWT过滤
        filters.put("jwt", new JwtFilter());

        return filters;
    }

    /**
     *   软件授权 拦截器
     * @return com.liu.filter.LicenseFilter
     */
    private LicenseFilter getLicenseFilter() {
        return new LicenseFilter();
    }

    /**
     *   访问 权限 拦截器
     * @return com.liu.filter.URLPathMatchingFilter
     */
    private URLPathMatchingFilter getURLPathMatchingFilter() {
        return new URLPathMatchingFilter();
    }

    /**
     * shiro身份认证realm,(自己实现:帐号密码校验、权限等)
     * @author : 64998
     * @date : 2019/5/20

    * @return : com.liu.shiro.MyRealm
     */
    @Bean
    public MyRealm myRealm(){
        MyRealm myRealm;
        myRealm = new MyRealm();
        //自定义的shiro密码校验器:
//        myRealm.setCredentialsMatcher(new CustomCredentialsMatcher());
        return myRealm;
    }

    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm());
        securityManager.setRememberMeManager(rememberMeManager());

        //关闭shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
        return securityManager;
    }

    /**
     * 记住我
     * @author : 64998
     * @date : 2019/5/21

     * @return : org.apache.shiro.web.mgt.CookieRememberMeManager
     */
    @Bean
    public CookieRememberMeManager rememberMeManager(){
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        return cookieRememberMeManager;
    }

    /**
     * cookie对象,记住密码实现
     * @author : 64998
     * @date : 2019/5/20

     * @return : org.apache.shiro.web.servlet.SimpleCookie
     */
    @Bean
    public SimpleCookie rememberMeCookie(){
        //这个参数是cookie的名称,对应前段的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //cookie记住我,生效时间,30天,单位秒
        simpleCookie.setMaxAge(259200);
        return simpleCookie;
    }

    /**
      * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),
      * 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
      * 与authorizationAttributeSourceAdvisor一起使用,否者shiro的注解不会生效
      * 相当于切面aop
      * @author kevin
      * @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
      * @date 2021/3/6 14:32
      */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     *  开启shiro aop注解支持,使用代理方式;所以需要开启代码支持;
     *  相当于切入点
     * @param securityManager :
     * @return org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * shiro生命周期处理器
     * @author : kevin
     * @date : 2019/5/20
     * @return : org.apache.shiro.spring.LifecycleBeanPostProcessor
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){

        return new LifecycleBeanPostProcessor();
    }

    /**
      * 异常处理
      * @author kevin
      * @return org.springframework.web.servlet.handler.SimpleMappingExceptionResolver
      * @date 2021/3/6 15:54
      */
    @Bean(name="simpleMappingExceptionResolver")
    public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
        Properties mappings = new Properties();
        mappings.setProperty("DatabaseException", "databaseError");//数据库异常处理
        mappings.setProperty("UnauthorizedException","403");
        r.setExceptionMappings(mappings);  // None by default
        r.setDefaultErrorView("error");    // No default
        r.setExceptionAttribute("ex");     // Default is "exception"
        //r.setWarnLogCategory("example.MvcLogger");     // No default
        return r;
    }
}

3.定义token实体继承shiro的token

import org.apache.shiro.authc.UsernamePasswordToken;

import java.io.Serializable;

public class JwtToken extends UsernamePasswordToken implements Serializable {

    private String token;

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

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

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

4.编写token处理工具类

此处token增加了sessionInfoMap,作用是将token与超时时间缓存到java内存,解决token本身无法续期问题。但是如果是分布式部署就无法保证token的有效性,可以将缓存中存放信息改存到redis中;如果数据库使用的一个的话,也可以存放到数据库中。

import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.commons.lang.time.DateUtils;

import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class JwtUtil {

    private static final int EXPIRE_TIME = 30;//分钟

    private static final int TOKEN_MAX_EXPIRE_TIME = 24*60;//分钟

    private static final String CLAIM_LOGIN_ID = "loginId";

    private static final String CLAIM_USER = "user";

    private static final ConcurrentMap sessionInfoMap = new ConcurrentHashMap<>();

    /**
     * 校验token是否正确
     * @author liuyong
     * @param token :
     * @param loginId :
     * @param secret :
     * @param sysUser :
     * @return boolean
     * @date 2020/12/23 16:41
     */
    public static boolean verify(String token, String loginId, String secret, JSONObject sysUser) {
        //根据密码生成JWT效验器
        Date now = new Date();
        Algorithm algorithm = Algorithm.HMAC256(secret);
        JWTVerifier verifier = JWT.require(algorithm)
                .withClaim(CLAIM_LOGIN_ID, loginId)
                .withClaim(CLAIM_USER, JSONObject.toJSONString(sysUser))
                .build();
        //效验TOKEN
        Date expireDate = (Date)sessionInfoMap.get(token);
        boolean afterNow = null != expireDate && expireDate.after(now);
        try {
            verifier.verify(token);
        }catch (TokenExpiredException e){
            if(!afterNow){
                //如果session超时,删除缓存中的session超时时间
                sessionInfoMap.remove(token);
                throw e;
            }
        }
        if(!afterNow){
            sessionInfoMap.remove(token);
            throw new TokenExpiredException(String.format("令牌已经在 %s 超时。", expireDate));
        }
        return true;
    }

    /**
     * 获得token中的信息无需secret解密也能获得
     * @author liuyong
     * @param token :
     * @return java.lang.String
     * @date 2020/12/23 16:41
     */
    public static String getLoginId(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(CLAIM_LOGIN_ID).asString();
        } catch (JWTDecodeException e) {
            return "";
        }
    }

    /**
     * 生成签名,30min后过期
     * @author liuyong
     * @param loginId :
     * @param secret :
     * @param sysUser :
     * @return java.lang.String
     * @date 2020/12/23 16:41
     */
    public static String sign(String loginId, String secret, JSONObject sysUser) {
        Date now = new Date();
        Date date = DateUtils.addMinutes(now, EXPIRE_TIME);
        Date tokenDate = DateUtils.addMinutes(now, TOKEN_MAX_EXPIRE_TIME);

        Algorithm algorithm = Algorithm.HMAC256(secret);
        // 附带username信息
        String token = JWT.create()
                .withClaim(CLAIM_LOGIN_ID, loginId)
                .withClaim(CLAIM_USER, JSONObject.toJSONString(sysUser))
                .withExpiresAt(tokenDate)
                .withIssuedAt(new Date())
                .sign(algorithm);
        sessionInfoMap.put(token, date);
        return token;
    }

    /**
     * 刷新token超时时间
     * @author liuyong
     * @param token :
     * @date 2020/12/24 10:13
     */
    public static void refreshToken(String token){
        Date now = new Date();
        Date date = DateUtils.addMinutes(now, EXPIRE_TIME);
        sessionInfoMap.put(token, date);
    }

    /**
     * 获取token超时时间
     * @author liuyong
     * @param token :
     * @return java.util.Date
     * @date 2020/12/24 11:35
     */
    public static Date getTokenExpireDate(String token){
        return (Date)sessionInfoMap.get(token);
    }

    /**
     * 清理已超时的session信息
     * @author liuyong
     * @date 2020/12/24 13:34
     */
    public static void cleanSessionInfo(){
        Date now = new Date();
        for (Map.Entry entry : sessionInfoMap.entrySet()) {
            Date expireDate = (Date) entry.getValue();
            if (!expireDate.after(now)) {
                sessionInfoMap.remove(entry.getKey());
            }
        }
    }

    /**
     * 根据token清除session信息
     * @author liuyong
     * @param token :
     * @date 2020/12/24 17:16
     */
    public static void cleanSessionInfo(String token) {
        sessionInfoMap.remove(token);
    }
}

5.自定义shiro的realm

import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.liu.domain.menu.Menu;
import com.liu.domain.role.Role;
import com.liu.service.manage.ManageService;
import com.liu.utils.ThreadLocals;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.context.annotation.Lazy;
import org.springframework.util.StringUtils;

import java.util.List;

/**
 * 自己实现登录授权处理
 *
 * @author kevin
 * @date 2019/5/18
 */
public class MyRealm extends AuthorizingRealm{
    @Autowired
    @Lazy
    private ManageService manageService;

    /**
     * 必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {

        return token instanceof JwtToken;
    }

    /**
     * 角色权限和对应权限添加
     * @author : 64998
     * @date : 2019/6/27
     * @param principals :
     * @return : org.apache.shiro.authz.AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String tokenStr  = principals.toString();
        String loginId = JwtUtil.getLoginId(tokenStr);
        JSONObject user = manageService.getUserByLoginId(loginId);

        List roles = manageService.getRoleByUserId(user.getInteger("uerId"));
        SimpleAuthorizationInfo simpleAuthenticationInfo = new SimpleAuthorizationInfo();
        for(Role role:roles){
            simpleAuthenticationInfo.addRole(role.getRoleType());
            List menus = manageService.getMenuByRoleId(role.getRoleId());
            for(Menu menu : menus){
                simpleAuthenticationInfo.addStringPermission(menu.getUrl());
            }
        }

        return simpleAuthenticationInfo;
    }

    /**
     * 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确。
     * @author : 64998
     * @date : 2019/5/18
     * @param authToken :
     * @return org.apache.shiro.authc.AuthenticationInfo
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authToken) {
        String token = (String) authToken.getPrincipal();
        if (StringUtils.isEmpty(token)) {
            throw new AuthenticationException("未找到token,请重新登录");
        }

        // 解密获得loginId,用于和数据库进行对比
        // 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        String loginId = JwtUtil.getLoginId(token);
        if (StringUtils.isEmpty(loginId)) {
            throw new AuthenticationException("token失效,请重新登录");
        }

        //此处查询自己的用户信息(根据自己的项目做调整)
        JSONObject userInfo = manageService.getUserByLoginId(loginId);
        if (userInfo == null) {
            throw new AuthenticationException("用户不存在!");
        }

        JSONObject sysUser = new JSONObject();
        sysUser.put("loginId", userInfo.get("loginId"));
        sysUser.put("userName", userInfo.get("userName"));
        sysUser.put("userId", userInfo.get("userId"));
        sysUser.put("sex", userInfo.get("sex"));
        sysUser.put("tel", userInfo.get("tel"));
        sysUser.put("isEnable", userInfo.get("isEnable"));

        try {
            //校验token是否有效,是否超时
            boolean isValid = JwtUtil.verify(token, loginId, userInfo.getString("password"), sysUser);
            //token验证成功,设置当前用户信息、更新token超时时间
            if(isValid) {
                JwtUtil.refreshToken(token);
                //此处使用ThreadLocal记录当前用户,没仔细处理,ThreadLocal应该还会有一些小bug
                ThreadLocals.setCurrentUserThl(sysUser);
            }else{
                throw new AuthenticationException("token验证失败,请重新登录");
            }
        }catch (TokenExpiredException e){
            throw new AuthenticationException("token已过期,请重新登录");
        }catch (Exception e){
            throw new AuthenticationException("用户名或密码错误");
        }

        //参数分别为用户帐号、token、realm name
        return new SimpleAuthenticationInfo(loginId, token, getName());
    }

    /**
     * 缓存的清除
     * @author : 64998
     * @date : 2019/6/27
     * @param principals :
     * @return : void
     */
    @Override
    protected void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
    }
}

6.JWTfilter过滤器,处理权限验证等

import com.liu.enums.ResponseState;
import com.liu.vo.resp.common.ResponseVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * JWT过滤器
 * @author 64998
 * @date 2019/5/21
 **/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {

    /**
     * 请求是否已经登录(携带token)
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader("token");
        return authorization != null;
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //每次都进入 executeLogin 方法执行登入,检查 token 是否正确
        try {
            executeLogin(request, response);
            return true;
        } catch (Exception e) {
            //token校验失败后,返回false调用onAccessDenied
            String msg;
            if(e instanceof AuthenticationException){
                msg = e.getMessage();
            }else{
                msg = "令牌无效或已过期,请重新登录!";
            }
            ResponseVo vo = new ResponseVo.Builder().error().message(msg).build();
            HttpServletResponse httpServletResponse = (HttpServletResponse)response;
            LoginUtils.responseOutJson(httpServletResponse,vo);
            return false;
        }
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("token");
        JwtToken jwtToken = new JwtToken(token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwtToken);
        jwtToken.setRememberMe(true);

        return true;
    }

    /**
     * 如果权限验证失败,则进入此方法
     * @author : 64998
     * @date : 2019/5/23
     * @param request:
     * @param response:
     * @return : boolean
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        ResponseVo vo = new ResponseVo();
        vo.setCode(ResponseState.LOGIN_ERROR.getCode());
        vo.setMsg("Token令牌无效或已过期,请重新登录!");
        HttpServletResponse httpServletResponse = (HttpServletResponse)response;
        LoginUtils.responseOutJson(httpServletResponse,vo);
        return false;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "*");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 处理非法请求返回
     * @author : 64998
     * @date : 2019/5/23
     * @param request:
     * @param response:
     * @param code:
     * @param msg:
     * @return : void
     */
    private void response401(ServletRequest request, ServletResponse response,int code,String msg) {
        try {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setStatus(HttpStatus.OK.value());
            response.setContentType("application/json;charset=utf-8");
            httpResponse.getWriter().write("{\"code\":" + code + ", \"msg\":\"" + msg + "\"}");
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
}

7.登出过滤器

import com.liu.vo.resp.common.ResponseVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * JWT登出重写
 * @author 64998
 * @Date 2019/5/21
 **/
@Slf4j
public class JwtLogoutFilter extends LogoutFilter {

    public JwtLogoutFilter(){

    }

    /**
     * 自定义登出,登出之后,清理当前用户缓存信息
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {

        // 登出操作 subject.logout() 可以自动清理缓存信息,
        JwtUtil.cleanSessionInfo(((HttpServletRequest)request).getHeader("token"));
        //这些代码是可以省略的  这里只是做个笔记 表示这种方式也可以清除
        Subject subject = getSubject(request,response);
        DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
        MyRealm myRealm = (MyRealm) securityManager.getRealms().iterator().next();
        PrincipalCollection principals = subject.getPrincipals();
        myRealm.clearCache(principals);
        //清除cookie
        Cookie cookie = new Cookie("token",null);
        cookie.setMaxAge(0);
        ((HttpServletResponse)response).addCookie(cookie);
        //登出
        subject.logout();
        ResponseVo vo = new ResponseVo();
        vo.setCode(200);
        vo.setMsg("登出成功!");
        HttpServletResponse httpServletResponse = (HttpServletResponse)response;
        LoginUtils.responseOutJson(httpServletResponse,vo);
        return false;
    }
}

项目地址:https://download.csdn.net/download/liu649983697/11227253

你可能感兴趣的:(springboot,java,spring,boot,jwt,shiro,tokenization,java)