美文网首页
shiro的jwt认证实现

shiro的jwt认证实现

作者: 东本三月 | 来源:发表于2020-09-11 17:25 被阅读0次

    1.环境

    • spring boot 版本 2.1.9.RELEASE
    • mybatis-plus 版本 3.2.0
     <dependencies>
            <!--shiro核心-->
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-core</artifactId>
                <version>1.4.0</version>
            </dependency>
    
            <!--shiro与spring整合依赖-->
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring</artifactId>
                <version>1.4.0</version>
            </dependency>
    
           <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>3.10.2</version>
            </dependency>
        </dependencies>
    

    个人学习总结,仅供参考

    2.代码

    • ShiroConfig shiro主配置类
    import cn.hutool.core.collection.CollectionUtil;
    import com.f4Blog.auth.interceptor.ShiroJwtFilter;
    import com.f4Blog.auth.shiro.ShiroRealm;
    import com.f4Blog.basic.util.SpringUtils;
    import com.f4Blog.model.constant.LoginConstant;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
    import org.apache.shiro.mgt.SessionStorageEvaluator;
    import org.apache.shiro.session.mgt.DefaultSessionManager;
    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.apache.shiro.web.mgt.DefaultWebSessionStorageEvaluator;
    import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.boot.web.servlet.FilterRegistrationBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.DependsOn;
    import org.springframework.web.filter.DelegatingFilterProxy;
    
    import org.apache.shiro.mgt.DefaultSubjectDAO;
    
    import javax.servlet.Filter;
    import java.util.HashMap;
    import java.util.LinkedHashMap;
    import java.util.List;
    import java.util.Map;
    
    /**
     * Shiro配置类
     */
    @Configuration
    //要求首先把上下文支持组件注册到spring
    @DependsOn("springUtils")
    public class ShiroConfig {
    
        /**
         *  创建shiro的过滤器工厂bean
         * @return
         */
        public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager")DefaultWebSecurityManager defaultWebSecurityManager){
            ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
            //设置安全管理器
            shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
            return shiroFilterFactoryBean;
        }
    
    
        /**
         * 禁用session功能
         * @return
         */
        @Bean
        public DefaultSessionManager sessionManager() {
            DefaultSessionManager manager = new DefaultSessionManager();
            manager.setSessionValidationSchedulerEnabled(false);
            return manager;
        }
    
        @Bean
        public SessionStorageEvaluator sessionStorageEvaluator() {
            DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
            sessionStorageEvaluator.setSessionStorageEnabled(false);
            return sessionStorageEvaluator;
        }
    
        @Bean
        public DefaultSubjectDAO subjectDAO() {
            DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
            defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
            return defaultSubjectDAO;
        }
    
    
        /**
         * 创建默认的 安全管理类
         * @return
         */
        @Bean("defaultWebSecurityManager")
        public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm){
            DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
            //关联Realm
            securityManager.setRealm(shiroRealm);
            securityManager.setSubjectDAO(subjectDAO());
            securityManager.setSessionManager(sessionManager());
            SecurityUtils.setSecurityManager(securityManager);
            return securityManager;
        }
    
    
        /**
         * 在方法中 注入 securityManager,进行代理控制
         */
        @Bean
        public MethodInvokingFactoryBean methodInvokingFactoryBean(DefaultWebSecurityManager securityManager) {
            MethodInvokingFactoryBean bean = new MethodInvokingFactoryBean();
            bean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
            bean.setArguments(securityManager);
            return bean;
        }
    
    
        /**
         * 缓存管理器 使用Ehcache实现
         */
    //    @Bean
    //    public CacheManager getCacheShiroManager(EhCacheManagerFactoryBean ehcache) {
    //        EhCacheManager ehCacheManager = new EhCacheManager();
    //        ehCacheManager.setCacheManager(ehcache.getObject());
    //        return ehCacheManager;
    //    }
    
        /**
         * Shiro生命周期处理器:
         * 用于在实现了Initializable接口的Shiro bean初始化时调用Initializable接口回调(例如:UserRealm)
         * 在实现了Destroyable接口的Shiro bean销毁时调用 Destroyable接口回调(例如:DefaultSecurityManager)
         */
        @Bean
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
    
        /**
         * 启用shrio授权注解拦截方式,AOP式方法级权限检查
         */
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
                    new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    
    
        /**
         * Shiro的过滤器链
         */
        @Bean
        public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
            //获取配置文件设置
            String loginUrl=SpringUtils.getEnvironment().getProperty(LoginConstant.LOGIN_URL_KEY);
            String successUrl=SpringUtils.getEnvironment().getProperty(LoginConstant.LOGIN_SUCCESS_URL_KEY);
            String unauthorizedUrl=SpringUtils.getEnvironment().getProperty(LoginConstant.UNAUTHORIZED_URL_KEY);
            String anonUrl=SpringUtils.getEnvironment().getProperty(LoginConstant.LOGIN_INTERCEPTOR_ANON_KEY,LoginConstant.LOGIN_INTERCEPTOR_ANON_DEFAULT);
            String[] anonUrls=anonUrl.split(",");
    
            //配置shiro过滤器工厂
            ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
            shiroFilter.setSecurityManager(securityManager);
            /**
             * 默认的登陆访问url
             */
            shiroFilter.setLoginUrl(loginUrl);
            /**
             * 登陆成功后跳转的url
             */
            shiroFilter.setSuccessUrl(successUrl);
            /**
             * 没有权限跳转的url
             */
            shiroFilter.setUnauthorizedUrl(unauthorizedUrl);
    
            //当运行一个Web应用程序时,Shiro将会创建一些有用的默认Filter实例,并自动地在[main]项中将它们置为可用自动地可用的默认的Filter实例是被DefaultFilter枚举类定义的,枚举的名称字段就是可供配置的名称
            /**
             * anon---------------org.apache.shiro.web.filter.authc.AnonymousFilter 没有参数,表示可以匿名使用。
             * authc--------------org.apache.shiro.web.filter.authc.FormAuthenticationFilter 表示需要认证(登录)才能使用,没有参数
             * authcBasic---------org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter 没有参数表示httpBasic认证
             * logout-------------org.apache.shiro.web.filter.authc.LogoutFilter
             * noSessionCreation--org.apache.shiro.web.filter.session.NoSessionCreationFilter
             * perms--------------org.apache.shiro.web.filter.authz.PermissionAuthorizationFilter 参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
             * port---------------org.apache.shiro.web.filter.authz.PortFilter port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
             * rest---------------org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter 根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
             * roles--------------org.apache.shiro.web.filter.authz.RolesAuthorizationFilter 参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
             * ssl----------------org.apache.shiro.web.filter.authz.SslFilter 没有参数,表示安全的url请求,协议为https
             * user---------------org.apache.shiro.web.filter.authz.UserFilter 没有参数表示必须存在用户,当登入操作时不做检查
             */
    
            /**
             * 通常可将这些过滤器分为两组
             * anon,authc,authcBasic,user是第一组认证过滤器
             * perms,port,rest,roles,ssl是第二组授权过滤器
             * 注意user和authc不同:当应用开启了rememberMe时,用户下次访问时可以是一个user,但绝不会是authc,因为authc是需要重新认证的
             * user表示用户不一定已通过认证,只要曾被Shiro记住过登录状态的用户就可以正常发起请求,比如rememberMe 说白了,以前的一个用户登录时开启了rememberMe,然后他关闭浏览器,下次再访问时他就是一个user,而不会authc
             *
             *
             * 举几个例子
             *  /admin=authc,roles[admin]      表示用户必需已通过认证,并拥有admin角色才可以正常发起'/admin'请求
             *  /edit=authc,perms[admin:edit]  表示用户必需已通过认证,并拥有admin:edit权限才可以正常发起'/edit'请求
             *  /home=user     表示用户不一定需要已经通过认证,只需要曾经被Shiro记住过登录状态就可以正常发起'/home'请求
             */
    
    
    
            /**
             * 覆盖默认的user拦截器
             */
            HashMap<String, Filter> myFilters = new HashMap<>();
            myFilters.put("user", new ShiroJwtFilter());
            shiroFilter.setFilters(myFilters);
    
            /**
             * 各默认过滤器常用如下(注意URL Pattern里用到的是两颗星,这样才能实现任意层次的全匹配)
             * /admins/**=anon             无参,表示可匿名使用,可以理解为匿名用户或游客
             *  /admins/user/**=authc       无参,表示需认证才能使用
             *  /admins/user/**=authcBasic  无参,表示httpBasic认证
             *  /admins/user/**=ssl         无参,表示安全的URL请求,协议为https
             *  /admins/user/**=perms[user:add:*]  参数可写多个,多参时必须加上引号,且参数之间用逗号分割,如/admins/user/**=perms["user:add:*,user:modify:*"]。当有多个参数时必须每个参数都通过才算通过,相当于isPermitedAll()方法
             *  /admins/user/**=port[8081] 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString。其中schmal是协议http或https等,serverName是你访问的Host,8081是Port端口,queryString是你访问的URL里的?后面的参数
             *  /admins/user/**=rest[user] 根据请求的方法,相当于/admins/user/**=perms[user:method],其中method为post,get,delete等
             *  /admins/user/**=roles[admin]  参数可写多个,多个时必须加上引号,且参数之间用逗号分割,如:/admins/user/**=roles["admin,guest"]。当有多个参数时必须每个参数都通过才算通过,相当于hasAllRoles()方法
             *
             */
    
    
            //Shiro验证URL时,URL匹配成功便不再继续匹配查找(所以要注意配置文件中的URL顺序,尤其在使用通配符时)
    
            //配置过滤链
            Map<String, String> hashMap = new LinkedHashMap<>();
            for (String nonePermissionRe : anonUrls) {
                hashMap.put(nonePermissionRe, "anon");
            }
            hashMap.put("/**", "user");
            shiroFilter.setFilterChainDefinitionMap(hashMap);
            return shiroFilter;
        }
    
    
            //解决UnavailableSecurityManagerException
        @Bean
        public FilterRegistrationBean delegatingFilterProxy(){
            FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
            DelegatingFilterProxy proxy = new DelegatingFilterProxy();
            proxy.setTargetFilterLifecycle(true);
            proxy.setTargetBeanName("shiroFilter");
            filterRegistrationBean.setFilter(proxy);
            return filterRegistrationBean;
        }
    
    
        /**
         * 创建Realm
         * @return
         */
        @Bean("shiroRealm")
        public ShiroRealm getShiroRealm(){
            return new ShiroRealm();
        }
    
        //自动创建代理类
        @Bean
        @ConditionalOnMissingBean
        public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
            daap.setProxyTargetClass(true);
            return daap;
        }
    }
    

    比较重要的代码一是禁用session,二是过滤器的设置
    尝试了几种禁用session的方式,感觉这种比较合适些

     /**
         * 禁用session功能
         * @return
         */
        @Bean
        public DefaultSessionManager sessionManager() {
            DefaultSessionManager manager = new DefaultSessionManager();
            manager.setSessionValidationSchedulerEnabled(false);
            return manager;
        }
    
        @Bean
        public SessionStorageEvaluator sessionStorageEvaluator() {
            DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
            sessionStorageEvaluator.setSessionStorageEnabled(false);
            return sessionStorageEvaluator;
        }
    
        @Bean
        public DefaultSubjectDAO subjectDAO() {
            DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
            defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
            return defaultSubjectDAO;
        }
    

    在ShiroConfig一般只配置一些通用的过滤约束,具体约束还是要通过注解设置.
    处于多模块共用的考虑,过滤的url是从配置文件中获取的.类头的DependsOn注解是为此添加的.

    //要求首先把上下文支持组件注册到spring
    @DependsOn("springUtils")
    

    • shiroRealm 处理认证与授权逻辑
    import com.f4Blog.auth.jwt.ShiroJwtToken;
    import com.f4Blog.auth.jwt.JwtUtil;
    import com.f4Blog.auth.service.imip.UserAuthServiceImpl;
    import com.f4Blog.auth.service.inter.UserAuthService;
    import com.f4Blog.basic.util.ToolUtil;
    import com.f4Blog.model.base.BaseUser;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.CredentialsException;
    import org.apache.shiro.authc.SimpleAuthenticationInfo;
    import org.apache.shiro.authc.UnknownAccountException;
    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 java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    
    /**
     * 自定义realm
     */
    public class ShiroRealm extends AuthorizingRealm {
    
        /**
         * 使用jwt代替原生token
         *
         * @param token
         * @return
         */
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof ShiroJwtToken;
        }
    
            /**
             * 执行授权逻辑
             * @param principalCollection
             * @return
             */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            System.out.println("执行授权逻辑");
            return null;
        }
    
        /**
         * 执行登录认证逻辑
         * @param authenticationToken
         * @return
         * @throws AuthenticationException
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            System.out.println("执行认证逻辑");
            UserAuthService shiroFactory = UserAuthServiceImpl.me();
    
            String token = (String)authenticationToken.getCredentials();
    
            String username = JwtUtil.getUsername(token);
    
            BaseUser user = shiroFactory.user(username);
    
            ShiroUser shiroUser = shiroFactory.shiroUser(user);
            if(user == null) {
                throw new UnknownAccountException("账号不存在");
            }
    
            if(!JwtUtil.verify(token, user.getUsername(), user.getPassword())) {
                throw new CredentialsException("用户名/密码错误");
            }
            SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo(shiroUser, token,getName());
            return simpleAuthenticationInfo;
        }
    }
    

    认证的流程是
    1.拿到传入的token
    2.获取token的username,从数据库拿到用户对象
    3.token与用户对象对比,验证token正确性
    4.返回shiro授权对象.
    需要注意的是,是不需要SimpleAuthenticationInfo进行认证效验的,效验逻辑是通过

            if(user == null) {
                throw new UnknownAccountException("账号不存在");
            }
    
            if(!JwtUtil.verify(token, user.getUsername(), user.getPassword())) {
                throw new CredentialsException("用户名/密码错误");
            }
    

    实现的

    网上查到的资料,一般都是返回的

    new SimpleAuthenticationInfo(token, token,getName());
    

    其目的是让shiro效验恒通过,但是会有一个问题,在其他地方去getCredentials()只能拿到token,拿不到当前登录对信息.
    自己做了些修改, 第一个参数设置为创建出的shiroUser 登录对象.
    采取jwt的验证,supports方法是必须重写的

       /**
         * 使用jwt代替原生token
         *
         * @param token
         * @return
         */
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof ShiroJwtToken;
        }
    

    ShiroJwtToken 代替shiro的token

    import org.apache.shiro.authc.AuthenticationToken;
    
    public class ShiroJwtToken implements AuthenticationToken {
        private static final long serialVersionUID = 1L;
    
        // 密钥
        private String token;
    
        public ShiroJwtToken(String token) {
            this.token = token;
        }
    
        @Override
        public Object getPrincipal() {
            return token;
        }
    
        @Override
        public Object getCredentials() {
            return token;
        }
    
    }
    
    

    JwtUtil 工具类,提供一些常用的token的操作方法

    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.interfaces.DecodedJWT;
    import org.springframework.util.StringUtils;
    
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.Date;
    
    
    public class JwtUtil {
    
    
        //短 过期时间60分钟
        private static final long SHORT_EXPIRE_TIME = 1000 * 60 * 60;
    
        //长 过期时间7天
        private static final long LONG_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7;
    
        //刷新token的时间节点,token有效时间低于该时间将刷新token  15分钟
        private static final long REFRESH_COUNT_DOWN = 1000 * 60 * 15;
    
    
        /**
         * 校验token是否正确
         *
         * @param token    密钥
         * @param username 登录名
         * @param password 密码
         * @return
         */
        public static boolean verify(String token, String username, String password) {
            try {
                Algorithm algorithm = Algorithm.HMAC256(password);
    
                JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
    
                DecodedJWT jwt = verifier.verify(token);
    
                return true;
            } catch (Exception e) {
                return false;
            }
        }
    
        /**
         * 获取用户名
         *
         * @param credentials
         * @return
         */
        public static String getUsername(String credentials) {
            try {
                DecodedJWT jwt = JWT.decode(credentials);
    
                return jwt.getClaim("username").asString();
            } catch (JWTDecodeException e) {
                return null;
            }
        }
    
        /**
         * 生成签名
         *
         * @param username
         * @param password
         * @return
         */
        public static String sign(String username, String password) {
            // 指定过期时间
            Date date = new Date(System.currentTimeMillis() + SHORT_EXPIRE_TIME);
    
            Algorithm algorithm = Algorithm.HMAC256(password);
    
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
        }
    
        /**
         * token的剩余有效期,单位毫秒
         * @param token
         * @return
         */
        public static Long getRemainingTime(String token) {
            try{
                DecodedJWT jwt = JWT.decode(token);
                Long remainingTime=jwt.getExpiresAt().getTime()-System.currentTimeMillis();
                return remainingTime;
            }catch (Exception e){
                return -1l;
            }
        }
    
        /**
         * 是否应刷新tonken
         * @param token
         * @return
         */
        public static Boolean isRefreshToken(String token){
            Long remainingTime=getRemainingTime(token);
            return null!=remainingTime && remainingTime>0 && remainingTime<=REFRESH_COUNT_DOWN;
        }
    
        /**
         * 在cookie和相应头设置token
         * @param response
         * @param token
         */
        public static void cookieAndHeaderSetToken(HttpServletResponse response,String token){
            Cookie cookie = new Cookie("token",token);
            cookie.setPath( "/");
            response.addCookie(cookie);
            response.setHeader("token",token);
        }
    
        /**
         * 在cookie和相应头 获取 token
         * @param httpServletRequest
         */
        public static String  cookieAndHeaderGetToken(HttpServletRequest  httpServletRequest){
            //获取请求头的token
            String token = httpServletRequest.getHeader("token");
            if(!StringUtils.hasText(token)){
                //请求头没有,尝试从cookie获取token
                Cookie[] cookies=httpServletRequest.getCookies();
                if(null!=cookies&&cookies.length!=0){
                    i:for(Cookie cookie:cookies){
                        if(cookie.getName().equalsIgnoreCase("token")){
                            token=cookie.getValue();
                            break i;
                        }
                    }
                }
            }
            return token;
        }
    }
    

    ShiroUser 实体类,用于记录当前登录对象信息

    import java.io.Serializable;
    import java.util.List;
    
    public class ShiroUser implements Serializable {
        private static final long serialVersionUID = 1L;
    
        /**
         * 用户主键ID
         */
        private Long id;
    
        /**
         * 账号
         */
        private String username;
    
        /**
         * 角色id 集合
         */
        private List<Long> roleList;
    
        /**
         * 角色名称 集合
         */
        private List<String> roleNames;
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public List<Long> getRoleList() {
            return roleList;
        }
    
        public void setRoleList(List<Long> roleList) {
            this.roleList = roleList;
        }
    
        public List<String> getRoleNames() {
            return roleNames;
        }
    
        public void setRoleNames(List<String> roleNames) {
            this.roleNames = roleNames;
        }
    }
    

    ShiroJwtFilter 过滤器,用于进行token的验证

    import com.f4Blog.auth.jwt.JwtUtil;
    import com.f4Blog.auth.jwt.ShiroJwtToken;
    import com.f4Blog.auth.service.imip.UserAuthServiceImpl;
    import com.f4Blog.auth.service.inter.UserAuthService;
    import com.f4Blog.basic.reqres.utils.WebUtils;
    import com.f4Blog.model.base.BaseUser;
    import org.apache.shiro.ShiroException;
    import org.apache.shiro.subject.Subject;
    import org.apache.shiro.web.filter.AccessControlFilter;
    import org.springframework.util.StringUtils;
    
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    
    /**
     * 进行用户的访问过滤
     * 通过继承AccessControlFilter的进行实现
     * https://www.cnblogs.com/CESC4/p/7599927.html
     * 抽象方法功能:
     * isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false;
     * onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理了,将直接返回即可。
     *
     * 父类提供的方法:
     * void setLoginUrl(String loginUrl) //身份验证时使用,默认/login.jsp
     * String getLoginUrl()
     * Subject getSubject(ServletRequest request, ServletResponse response) //获取Subject实例
     * boolean isLoginRequest(ServletRequest request, ServletResponse response)//当前请求是否是登录请求
     * void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException //将当前请求保存起来并重定向到登录页面
     * void saveRequest(ServletRequest request) //将请求保存起来,如登录成功后再重定向回该请求
     * void redirectToLogin(ServletRequest request, ServletResponse response) //重定向到登录页面
     */
    public class ShiroJwtFilter extends AccessControlFilter {
    
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            HttpServletRequest httpServletRequest =  (HttpServletRequest) request;
            HttpServletResponse httpServletResponse = (HttpServletResponse)response;
            if (isLoginRequest(request, response)) {
                return true;
            } else {
                //获取请求头的token
                String token = JwtUtil.cookieAndHeaderGetToken(httpServletRequest);
                Subject subject = getSubject(request, response);
                if(StringUtils.hasText(token)){
                    //使用token进行shiro认证
                    try {
                        subject.login(new ShiroJwtToken(token));
                    }catch (ShiroException e){
                        httpServletRequest.setAttribute("tips", "认证失效");
                        return false;
                    }
                    //判断是否应该刷新token
                    if(JwtUtil.isRefreshToken(token)){
                        try {
                            UserAuthService shiroFactory = UserAuthServiceImpl.me();
                            String username = JwtUtil.getUsername(token);
                            BaseUser user = shiroFactory.user(username);
                            String newToken = JwtUtil.sign(user.getUsername(), user.getPassword());
                            JwtUtil.cookieAndHeaderSetToken(WebUtils.getResponse(),newToken);
                        }catch (Exception e){
                            e.printStackTrace();
                            //日志 刷新token出错
                        }
                    }else{
                        //不刷新,将token重新放回响应头
                        httpServletResponse.setHeader("token",token);
                    }
                }else{
                    return false;
                }
    
                return true;
            }
        }
    
    
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
            HttpServletRequest httpServletRequest =  (HttpServletRequest) request;
            HttpServletResponse httpServletResponse = (HttpServletResponse)response;
    
            /**
             * 如果是ajax请求则不进行跳转
             */
            if (httpServletRequest.getHeader("x-requested-with") != null
                    && httpServletRequest.getHeader("x-requested-with").equalsIgnoreCase("XMLHttpRequest")) {
                return false;
            } else {
                httpServletResponse.sendRedirect(getLoginUrl());
    //            saveRequestAndRedirectToLogin(request, response);
            }
            return false;
        }
    }
    
    

    过滤器主要做的事是拿token进行shiro登录,生成用户信息和刷新token.
    我了解到的刷新token的方式有三种
    1.token时效极短,每次请求都会重新生成token,下次请求需要拿着新token才能通过认证
    2.在token即将失效时,刷新token
    3.添加一个refresh_token,时效较长,来声明刷新token的权限,对于快失效或者已经失效的token,会效验refresh_token然后判断是否刷新token.
    第一种安全性比较高,但是性能开销大.第三种比较繁琐没什么必要,我暂时采取的是第二种方式.


    UserAuthServiceImpl 为shiro提供数据库操作

    @Service
    //要求首先把上下文支持组件注册到spring
    @DependsOn("springUtils")
    @Transactional(readOnly = true)
    public class UserAuthServiceImpl implements UserAuthService {
    
        @Autowired
        private RoleDao roleDao;
    
    
        @Autowired
        private UserService userService;
    
    
        /**
         * 获取自身实例对象
         * @return
         */
        public static UserAuthService me() {
            return SpringUtils.getBean(UserAuthService.class);
        }
    
        @Override
        public BaseUser user(String account) {
            BaseUser user = userService.getByUsername(account);
            // 账号不存在
            if (null == user) {
                throw new CredentialsException();
            }
    //        // 账号被冻结
    //        if (!user.getStatus().equals(ManagerStatus.OK.getCode())) {
    //            throw new LockedAccountException();
    //        }
            return user;
        }
    
        @Override
        public ShiroUser shiroUser(BaseUser user) {
    
            ShiroUser shiroUser = createShiroUser(user);
    
            //用户的角色id集合
            List<Long> roleIds = roleDao.getRoleIdByUserId(user.getId());
    
            //用户的角色名称集合
            List<String> roleNames = roleDao.getRoleNameByUserId(user.getId());
    
            shiroUser.setRoleList(roleIds);
            shiroUser.setRoleNames(roleNames);
    
            return shiroUser;
        }
    
      
        @Override
        public SimpleAuthenticationInfo info(ShiroUser shiroUser, BaseUser user, String realmName) {
    
            // 密码加盐处理
            String salt = user.getSalt();
            String credentials = user.getPassword();
            ByteSource credentialsSalt = new Md5Hash(salt);
            return new SimpleAuthenticationInfo(shiroUser, credentials, credentialsSalt, realmName);
        }
    
        /**
         * 通过用户表的信息创建一个shiroUser对象
         */
        public static ShiroUser createShiroUser(BaseUser user) {
            ShiroUser shiroUser = new ShiroUser();
    
            if (user == null) {
                return shiroUser;
            }
    
            shiroUser.setId(user.getId());
            shiroUser.setUsername(user.getUsername());
            return shiroUser;
        }
    }
    

    登录controller相关方法

     @RequestMapping(value ="/login",method = RequestMethod.GET)
        public String login(ModelAndView model) {
            String token=JwtUtil.cookieAndHeaderGetToken(WebUtils.getRequest());
            if(StringUtils.hasText(token)){
                try{
                    SecurityUtils.getSubject().login(new ShiroJwtToken(token));
                    return REDIRECT+"pages/test/index";
                } catch (ShiroException e){
                }
            }
            return "/login";
        }
    
        @RequestMapping(value ="/login",method = RequestMethod.POST)
        public String loginVail(Model model){
            //验证是否已登录
            String username= WebUtils.get("username");
            String password=WebUtils.get("password");
    
            BaseUser user = userService.getByUsername(username);
            String md5_password=ShiroKit.md5(password ,user.getSalt());
            String token = JwtUtil.sign(user.getUsername(), md5_password);
    
            SecurityUtils.getSubject().login(new ShiroJwtToken(token));
            JwtUtil.cookieAndHeaderSetToken(WebUtils.getResponse(),token);
    
            model.addAttribute("token",token);
    
            //登录成功,记录登录日志
    
            return REDIRECT+"pages/test/index";
        }
    
    
    
    
        //退出
        @RequestMapping("/logout")
        public String logout() {
    //        SecurityUtils.getSubject().logout();
    //        移除Cookie中的token
            Cookie cookie = new Cookie("token","");
            cookie.setValue(null);
            cookie.setMaxAge(0);
            cookie.setPath( "/");
            WebUtils.getResponse().addCookie(cookie);
            return REDIRECT+"/login";
        }
    

    一些辅助类
    GlobalExceptionHandler 全局异常捕获

    
    /**
     * 全局的的异常拦截器(拦截所有的控制器)(带有@RequestMapping注解的方法上都会拦截)
     *
     */
    @ControllerAdvice
    /**
     *ControllerAdvice注解 用于对Controller进行“切面”环绕
     * 其用法主要有三点:
     * 结合方法型注解@ExceptionHandler,用于捕获Controller中抛出的指定类型的异常,从而达到不同类型的异常区别处理的目的;
     * 结合方法型注解@InitBinder,用于request中自定义参数解析方式进行注册,从而达到自定义指定格式参数的目的;
     * 结合方法型注解@ModelAttribute,表示其标注的方法将会在目标Controller方法执行之前执行。
     */
    
    @Order(-1)
    /**
     *注解@Order的作用是定义Spring IOC容器中Bean的执行顺序的优先级,而不是定义Bean的加载顺序
     * 默认是最低优先级,值越小优先级越高
     */
    public class GlobalExceptionHandler {
    
        private Logger log = LoggerFactory.getLogger(this.getClass());
    
        /**
         * 用户未登录异常
         */
        @ExceptionHandler(AuthenticationException.class)
        @ResponseStatus(HttpStatus.UNAUTHORIZED)
        public String unAuth(AuthenticationException e) {
            log.error("用户未登陆:", e);
            return "/login";
        }
    
        /**
         * 账号被冻结异常
         */
        @ExceptionHandler(DisabledAccountException.class)
        @ResponseStatus(HttpStatus.UNAUTHORIZED)
        public String accountLocked(DisabledAccountException e, Model model) {
            String username = getRequest().getParameter("username");
    //        LogManager.me().executeLog(LogTaskFactory.loginLog(username, "账号被冻结", getIp()));
            model.addAttribute("tips", "账号被冻结");
            return "/login";
        }
    
        /**
         * 账号密码错误异常
         */
        @ExceptionHandler(CredentialsException.class)
        @ResponseStatus(HttpStatus.UNAUTHORIZED)
        public String credentials(CredentialsException e, Model model) {
    //        String username = getRequest().getParameter("username");
    //        LogManager.me().executeLog(LogTaskFactory.loginLog(username, "账号密码错误", getIp()));
            model.addAttribute("tips", "账号密码错误");
            return "/login";
        }
    
        /**
         * 验证码错误异常
         */
        @ExceptionHandler(InvalidKaptchaException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public String credentials(InvalidKaptchaException e, Model model) {
            String username = getRequest().getParameter("username");
    //        LogManager.me().executeLog(LogTaskFactory.loginLog(username, "验证码错误", getIp()));
            model.addAttribute("tips", "验证码错误");
            return "/login";
        }
    
        /**
         * 无权访问该资源异常
         */
        @ExceptionHandler(UndeclaredThrowableException.class)
        @ResponseStatus(HttpStatus.UNAUTHORIZED)
        @ResponseBody
        public ErrorResponseData credentials(UndeclaredThrowableException e) {
            getRequest().setAttribute("tip", "权限异常");
            log.error("权限异常!", e);
            return new ErrorResponseData(BaseExceptionEnum.NO_PERMITION.getCode(), BaseExceptionEnum.NO_PERMITION.getMessage());
        }
    
    
    
        /**
         * 捕获数据绑定异常
         * @param ex
         * @return
         */
        @ExceptionHandler(value={BindException.class, MethodArgumentNotValidException.class})
        @ResponseBody
        public ErrorResponseData bindExceptionHandler(Exception ex) {
            ErrorResponseData res=new ErrorResponseData();
            //出现参数不正确的异常,在返回值显示提示信息,具体错误消息进行记录和打印
            //设置为参数错误
            res.addEnumInfo(BaseExceptionEnum.PARA_ERROR);
            List<FieldError> fieldErrors=null;
            if(ex instanceof BindException){
                fieldErrors=((BindException)ex).getBindingResult().getFieldErrors();
            }else if(ex instanceof  MethodArgumentNotValidException){
                fieldErrors=((MethodArgumentNotValidException)ex).getBindingResult().getFieldErrors();
            }else{
                return res;
            }
            List<String> allError=new ArrayList<>();
            //将第一个错误信息作为响应的错误信息
            res.setMessage(fieldErrors.get(0).getDefaultMessage());
            //记录其他错误信息
            for (FieldError error:fieldErrors){
                allError.add(error.getDefaultMessage());
            }
            res.setData(allError);
            return res;
        }
    
    
        /**
         * 捕获消息型的异常
         * @param ex
         * @return
         */
        @ExceptionHandler(value={ServiceException.class})
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        @ResponseBody
        public ErrorResponseData exceptionHandler(ServiceException ex) {
            ErrorResponseData res=new ErrorResponseData();
            res.addExceptionInfo(ex);
            log.error("业务异常:", ex);
            return res;
        }
    
    
        /**
         * 捕获出错型的异常
         * @param ex
         * @return
         */
        @ExceptionHandler(value={ErrorException.class})
        @ResponseBody
        public ErrorResponseData exceptionHandler(ErrorException ex) {
            ErrorResponseData res=new ErrorResponseData();
            //出现错误型异常,在返回值显示服务器异常,具体错误消息进行记录和打印
            ex.printStackTrace();
            log.error("系统执行出现异常:" + ex.getMessage());
            return res;
        }
    
        /**
         * 捕获其他异常
         * @param ex
         * @return
         */
        @ExceptionHandler(value={Exception.class})
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        @ResponseBody
        public ErrorResponseData exceptionHandler(Exception ex) {
            ErrorResponseData res=new ErrorResponseData();
            //不确定的异常,在返回值显示未知异常,具体错误消息进行记录和打印
            res.addEnumInfo(BaseExceptionEnum.UNKNOWN_ERROR);
            ex.printStackTrace();
            this.log.error( "系统出现未知异常 :" + ex.getMessage());
            return res;
        }
    }
    
    

    3.后续需要考虑修改地方

    1.token验证增加缓存
    2.前端增加全局的设置token到header
    3.实现一个类似[7天内免登录]的效果
    4.增加错误页面

    相关文章

      网友评论

          本文标题:shiro的jwt认证实现

          本文链接:https://www.haomeiwen.com/subject/nthbektx.html