美文网首页
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