Java:SpringBoot+Jwt+Shiro 身份权限认证

在上文中描述了项目中MybatisPlus的代码生成功能。这里讲述Jwt+Shiro做身份和权限认证。
笔者用自然语言描述一个工作流程:用户访问项目资源时,默认是不允许游客访问的,需要用户身份才可以访问,所以需要用户登录,放回一个token给用户,当用户需要访问资源时在request中携带token。后端会对token进行身份认证,通过后才允许访问,否则返回错误。而且对特殊的资源还添加了权限,并不是所有的用户都可以访问我这些资源,只有被赋予了权限才可以访问,因此添加了权限认证。在项目中使用的机制:

  1. token签发和校验
  2. 游客资源放行
  3. 权限认证
  4. token刷新

首先贴出关键的代码:

  • ShiroConfig
package com.huafeng.cams.common.shiro;

import com.huafeng.cams.common.shiro.jwt.JwtFilter;
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.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * Author      JinXing     _让世界看到我
 * On          2020/4/22
 * Note        TODO
 */
@Configuration
public class ShiroConfig {

    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //设置我们自定义的过滤器
        Map<String, Filter> filters = new HashMap<>();
        filters.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filters);

        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //设置无权限时的跳转,好像没有效果
        //shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
        //设置放行的登录访问 *好像错误的访问也会跳转到这个访问位置
        //shiroFilterFactoryBean.setLoginUrl("/sysUser/login");

        /**
         * 设置过滤器
         * anon:表示可以匿名访问
         * authc:表示需要认证通过才可以访问
         */
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //测试不需要认证的的接口
        filterChainDefinitionMap.put("/common/**", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/swagger/**", "anon");
        filterChainDefinitionMap.put("/v2/api-docs", "anon");
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        filterChainDefinitionMap.put("/captcha.jpg", "anon");
        filterChainDefinitionMap.put("/sysUser/login", "anon");
        filterChainDefinitionMap.put("/*.html", "anon");

        //需要shiro认证的接口
        //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截,剩余的都需要认证.表示其它的所有接口都要经过我们的过滤器
        filterChainDefinitionMap.put("/**", "jwt");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
        defaultSecurityManager.setRealm(myRealm());
        return defaultSecurityManager;
    }

    @Bean
    public ShiroRealm myRealm() {
        ShiroRealm realm = new ShiroRealm();
        return realm;
    }

    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        //creator.setProxyTargetClass(true);
        return creator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

Shiro做权限控制的配置文件,这个中创建了一个filter对访问进行过滤,在过滤器中对用户的身份和权限进行认证。如果是非法的访问在这里就会被打回,不会到controller。同时创建这个filter时需要一些捆绑配置,按照配置文件写就可以了,基本上都是固定写法,不同版本可能有所区别,但是大同小异。其中myRealm()函数是指定一个自定义的realm,本文中也将realm代码块贴出。在下文会讲到。
重点是创建的filter,这个filter是我们自定义的,可以在这里对这个filter进行管理,比如什么访问会放行,什么访问要进filter。一般都是常用的写法,静态资源要放行,登录接口要放行,swagger要放行,这些如果没有添加进去使用过程中就会出现错误,所以注意一下就行。设置过滤器时注意这里:

* anon:表示可以匿名访问
* authc:表示需要认证通过才可以访问
  • JwtFilter
package com.huafeng.cams.common.shiro.jwt;

import com.google.gson.Gson;
import com.huafeng.cams.common.R;
import com.huafeng.cams.common.exception.MyException;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

public class JwtFilter extends BasicHttpAuthenticationFilter {

    private static Logger logger = LoggerFactory.getLogger(JwtUtil.class);

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        logger.info("JwtFilter.createToken");
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            return null;
        }
        return new JwtToken(token);
    }

    /**
     * 对访问进行拦截
     * @param request
     * @param response
     * @param mappedValue
     * @return true:放行,false拦截
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        logger.info("JwtFilter.isAccessAllowed");
        if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
            return true;
        }
        return false;
    }

    /**
     * isAccessAllowed拦截后会执行此函数
     * 在request中获取到token
     *  如果没有token直接响应客户端,并且返回false访问结束
     *  如果有token就执行executeLogin函数
     *      executeLogin函数不需要重写,后面会执行到自定义的realm中的doGetAuthenticationInfo身份认证函数
     *          如果身份认证通过就会继续后续的访问
     *              如果controller中有权限标识就会执行权限认证
     *              如果controller中没有权限标识就会直接进入controller
     *          如果身份认证没有通过executeLogin函数就会返回false,并且会执行onLoginFailure函数
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        logger.info("JwtFilter.onAccessDenied");
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            String result = new Gson().toJson(R.RESULT_ERROR("请先登录"));
            response(response,result);
            return false;
        }
        boolean login = executeLogin(request, response);
        if (login) {
            refreshToken(request, response);
        }
        return login;
    }

    /**
     * 身份信息认证成功后刷新token给客户端
     * 自定义的token刷新策略
     *
     * @param request
     * @param response
     */
    private void refreshToken(ServletRequest request, ServletResponse response) throws MyException {
        AuthenticationToken authenticationToken = createToken(request, response);
        String token = (String) authenticationToken.getCredentials();
        String newToken = JwtUtil.refreshToken(token);
        if (!StringUtils.isEmpty(newToken)){
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader(JwtUtil.TOKEN_TAG,newToken);
        }
    }

    /**
     * 这个函数标识token身份信息认证失败
     *  可以在AuthenticationException异常中拿到我们定义身份认证中抛出的异常,获取到异常信息返回给客户端
     * @param token
     * @param e 自定义realm中身份认证抛出的异常,可以从中获取到抛出的异常信息
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        logger.info("JwtFilter.onLoginFailure");
        //获取登录失败的异常的信息
        //e.printStackTrace();
        Throwable t = e.getCause() == null ? e : e.getCause();
        String msg = "用户信息验证失败!";
        if (t != null) {
            msg = t.getMessage();
        }
        String result = new Gson().toJson(R.RESULT_ERROR(msg));
        response(response,result);
        return false;
    }

    /**
     * 获取请求的token
     */
    private String getRequestToken(HttpServletRequest httpRequest) {
        //从header中获取token
        return httpRequest.getHeader(JwtUtil.TOKEN_TAG);
    }

    /**
     * filter中返回客户端
     * @param response
     * @param json
     */
    private void response(ServletResponse response, String json) {
        try {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setContentType("application/json;charset=utf-8");
            httpResponse.setCharacterEncoding("utf-8");
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Origin", "*");
            httpResponse.getWriter().print(json);
        } catch (IOException e) {
            logger.warn("filter响应错误:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

我们的过滤器要集成BasicHttpAuthenticationFilter,并覆写以下几个函数:

  1. AuthenticationToken createToken()
  2. boolean isAccessAllowed()
    没有被放行的访问首先会进入这个函数,询问是否拦截这个访问,true:放行,false:拦截。
  3. boolean onAccessDenied()
    被isAccessAllowed拦截的访问会执行到这里,在这里我们首先在request中获取用户携带的token,如果没有token就直接在这里返回提示登录,并结束这次访问。如果有token那么就对token进行验证,执行executeLogin()函数,这个函数不用覆写,系统会通过createToken()函数获取到token,将访问执行到realm中的AuthenticationInfo doGetAuthenticationInfo()身份认证函数中,如果身份认证没有异常就说明认证通过,反之就是验证失败。如果验证通过executeLogin()函数会返回true,同理onAccessDenied()也会返回true访问将继续进行,否则就是返回false。并且会执行onLoginFailure()。
  4. boolean onLoginFailure()
    在onAccessDenied()函数中返回false表示用户身份认证失败,即realm中身份认证失败,那么理论上会有失败的异常,如果没有异常就是系统的异常。在这里可以获取异常信息直接返回给客户端。并返回false结束这次访问。
  • JwtToken
package com.huafeng.cams.common.shiro.jwt;

import org.apache.shiro.authc.AuthenticationToken;

/**
 * Author      JinXing     _让世界看到我
 * On          2020/4/22
 * Note        JwtToken实现shiro的AuthenticationToken接口
 */
public class JwtToken implements AuthenticationToken {

    private String token;

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

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

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

这个token继承AuthenticationToken主要就是在身份认证中方便获取到token信息,一般就是固定写法。

  • JwtUtil
package com.huafeng.cams.common.shiro.jwt;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.huafeng.cams.common.exception.MyException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * Author      JinXing     _让世界看到我
 * On          2020/1/3
 * Note        token 工具
 */
public class JwtUtil {

    public static final String TOKEN_TAG = "token";

    private static Logger logger = LoggerFactory.getLogger(JwtUtil.class);

    //token 过期时间 单位ms
    public static final long EXPIRE_TIME = 1000 * 60 * 60;
    //token 私钥
    public static final String TOKEN_SECRET = "xxxxxx";

    public static final String TOKEN_KEY_USERNAME = "username";
    public static final String TOKEN_KEY_USER_ID = "userId";

    /**
     * 生成签名
     *
     * @param username
     * @param userId
     * @return 加密的token
     */
    public static String sign(String username, long userId) throws MyException {
        try {
            //过期时间
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            //私钥加密
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            //设置头信息
            Map<String, Object> map = new HashMap<>(2);
            map.put("typ", "jwt");
            map.put("alg", "HS256");

            String sign = JWT.create()
                    .withHeader(map)
                    .withClaim(TOKEN_KEY_USERNAME, username)
                    .withClaim(TOKEN_KEY_USER_ID, userId)
                    .withExpiresAt(date)
                    .sign(algorithm);
            return sign;
        } catch (Exception e) {
            throw new MyException("生成Token失败:" + e.getMessage());
        }
    }

    /**
     * 验证token
     *
     * @param token
     * @return
     */
    public static boolean verify(String token, String username, long id) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            JWTVerifier verifier = JWT
                    .require(algorithm)
                    .withClaim(TOKEN_KEY_USERNAME, username)
                    .withClaim(TOKEN_KEY_USER_ID, id)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception e) {
            logger.warn("token验证异常:" + e.getMessage());
        }
        return false;
    }

    /**
     * 解析token
     *
     * @param token
     * @param key
     * @return
     */
    public static Claim decodeToken(String token, String key) {
        if (token == null || key == null) {
            return null;
        }
        DecodedJWT jwt = JWT.decode(token);
        Claim claim = jwt.getClaim(key);
        return claim;
    }

    /**
     * 解析token
     *
     * @param token
     * @param key
     * @return
     */
    public static long decodeTokenAsLong(String token, String key) {
        if (token == null || key == null) {
            return -1;
        }
        try {
            DecodedJWT jwt = JWT.decode(token);
            Claim claim = jwt.getClaim(key);
            return claim.asLong();
        } catch (Exception e) {
            logger.warn("token decode failed!" + e.getMessage());
            return -1;
        }
    }

    /**
     * 解析token中的用户id
     *
     * @param token
     * @return
     */
    public static long getUserId(String token) {
        return decodeTokenAsLong(token, TOKEN_KEY_USER_ID);
    }

    /**
     * 解析token
     *
     * @param token
     * @param key
     * @return
     */
    public static String decodeTokenAsString(String token, String key) {
        if (token == null || key == null) {
            return null;
        }
        try {
            DecodedJWT jwt = JWT.decode(token);
            Claim claim = jwt.getClaim(key);
            return claim.asString();
        } catch (Exception e) {
            logger.warn("token decode failed!" + e.getMessage());
            return null;
        }
    }

    /**
     * 解析token中的用户名称
     *
     * @param token
     * @return
     */
    public static String getUsername(String token) {
        return decodeTokenAsString(token, TOKEN_KEY_USERNAME);
    }

    /**
     * 解析token
     *
     * @param token
     * @return
     */
    public static Map<String, Object> decodeTokenAsMap(String token) {
        Map<String, Object> map = new HashMap<>();
        if (token == null) {
            return null;
        }
        DecodedJWT jwt = JWT.decode(token);
        map.put(TOKEN_KEY_USERNAME, jwt.getClaim(TOKEN_KEY_USERNAME).asString());
        map.put(TOKEN_KEY_USER_ID, jwt.getClaim(TOKEN_KEY_USER_ID).asLong());
        return map;
    }

    /**
     * 刷新token
     * 获取一个token中的信息,创建一个新的token
     * @param token
     * @return
     */
    public static String refreshToken(String token) throws MyException {
        if (StringUtils.isEmpty(token)) {
            return null;
        }
        long userId = getUserId(token);
        String username = getUsername(token);
        if (userId!=-1&&username!=null){
            return sign(username, userId);
        }
        return null;
    }
}

token工具类,主要完成token的签发、校验和解码等。

  • ShiroRealm
package com.huafeng.cams.common.shiro;

import com.google.gson.Gson;
import com.huafeng.cams.common.shiro.jwt.JwtToken;
import com.huafeng.cams.common.shiro.jwt.JwtUtil;
import com.huafeng.cams.module.sys_user.entity.SysUser;
import com.huafeng.cams.module.sys_user.service.ISysUserService;
import org.apache.commons.lang3.StringUtils;
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.AuthorizationException;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.HashSet;
import java.util.Set;

/**
 * Author      JinXing     _让世界看到我
 * On          2020/4/22
 * Note        TODO
 */
public class ShiroRealm extends AuthorizingRealm {

    private static Logger logger = LoggerFactory.getLogger(ShiroRealm.class);

    @Autowired
    ISysUserService mUserService;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 只有当需要认证用户权限时才会调用此函数
     * 在controller的函数中使用@RequiresPermissions("艇员","系统管理员",logical = Logical.OR)注解,表示这个访问是要添加权限的,可以是单个,也可以是多个用逗号隔开,如果不加logical属性则是必须同时满足两个权限才可以访问,加了这个属性就是满足其一就行
     *  上述是采用权限的方式来验证,也可以使用@RequiresRoles(value = {"艇员","系统管理员"},logical = Logical.OR)注解采用角色的方式来认证,表示拥有这个角色的用户才可以访问,注解含义与上述相同,只不过一个是验证权限,一个是验证角色,采用的策略不同,可以根据场景来选择使用方式
     * 根据权限验证注解的添加,如果用户访问添加了权限验证的函数,就会执行此函数,在这里如果验证通过会访问controller,如果验证不能通过就会出现AuthorizationException异常,Subject does not have role提示用户没有这角色,或者没有这个权限.理论上是应该捕获这个异常对用户进行统一返回.
     * 上述是对controller中的函数添加权限控制,此函数中就是对应的权限验证策略
     * 函数中可以给simpleAuthorizationInfo对象添加角色和权限,返回后验证与控制器中添加的权限进行校验.可以根据使用场景做不同的权限控制.
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String token = principalCollection.toString();
        logger.info("权限认证:"+token);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        long userId = JwtUtil.getUserId(token);
        Set<String> roles = mUserService.getRoles(userId);
        Set<String> permissions = mUserService.getPermissions(userId);
        simpleAuthorizationInfo.setRoles(roles);
        simpleAuthorizationInfo.setStringPermissions(permissions);
        return simpleAuthorizationInfo;

    }

    /**
     * 默认使用此函数进行用户校验,如果有错误在这里抛出来即可
     * 自定义filter的executeLogin函数放行过来带token的访问
     * 在这里对token中的身份进行验证,如果此处验证token信息不正确可以直接抛出异常,然后在别的地方捕获掉异常对客户端进行返回(目前还没有做到捕获异常环节)
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
        String token = (String) authenticationToken.getCredentials();
        logger.info("身份认证:"+token);

        if (StringUtils.isEmpty(token)){
            throw new AuthorizationException("token null");
        }

        long userId = JwtUtil.getUserId(token);
        if (userId<0){
            throw new AuthorizationException("token invalid");
        }

        SysUser user = mUserService.getById(userId);
        if (user==null){
            throw new AuthorizationException("user invalid");
        }

        if (user.getcStatus()!=SysUser.STATUS_NORMAL){
            throw new AuthorizationException("用户状态异常");
        }

        if (!JwtUtil.verify(token,user.getUsername(),user.getId())) {
            throw new AuthorizationException("token lose");
        }
        return new SimpleAuthenticationInfo(token,token,getName());
    }
}

realm中自定义的身份认证和权限认证。继承AuthorizingRealm,覆写以下函数:

  1. boolean supports()
    有人说不写这个函数就会出错,所以就写上了,一般固定写法。
  2. AuthenticationInfo doGetAuthenticationInfo()
    身份认证函数,在filter中的onAccessDenied()函数中如果request中有token信息,就会执行executeLogin()函数,最终会执行到doGetAuthenticationInfo()函数,在这里获取到token信息,并进行一系列自定义的验证:
@Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
        String token = (String) authenticationToken.getCredentials();
        logger.info("身份认证:"+token);

        if (StringUtils.isEmpty(token)){
            throw new AuthorizationException("token null");
        }

        long userId = JwtUtil.getUserId(token);
        if (userId<0){
            throw new AuthorizationException("token invalid");
        }

        SysUser user = mUserService.getById(userId);
        if (user==null){
            throw new AuthorizationException("user invalid");
        }

        if (user.getcStatus()!=SysUser.STATUS_NORMAL){
            throw new AuthorizationException("用户状态异常");
        }

        if (!JwtUtil.verify(token,user.getUsername(),user.getId())) {
            throw new AuthorizationException("token lose");
        }

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

如果没有通过验证,直接在这里抛出异常即可,并且会执行到filter中的onLoginFailure()函数,在那里可以获取到这里抛出的异常信息并返回客户端等处理。

  1. AuthorizationInfo doGetAuthorizationInfo()
    权限认证的函数,在这里对用户的权限进行验证。这个函数不一定执行,当controller中的接口添加了@RequiresPermissions("menu:update")、@RequiresRoles("admin")等权限控制注解时,shiro就会在这里对用户进行权限认证,这里的认证规则是自定义的,业务不同也无法炮制。流程就是在这里获取到token信息,通过token获取用户的信息,根据用户信息获取用户角色信息,根据角色获取到该角色的权限,然后将权限和角色添加到鉴权对象中,由shiro去完成权限认证。
@Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String token = principalCollection.toString();
        logger.info("权限认证:"+token);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        long userId = JwtUtil.getUserId(token);
        Set<String> roles = mUserService.getRoles(userId);
        Set<String> permissions = mUserService.getPermissions(userId);
        simpleAuthorizationInfo.setRoles(roles);
        simpleAuthorizationInfo.setStringPermissions(permissions);
        return simpleAuthorizationInfo;
    }

如果权限认证失败,shiro会抛出AuthorizationException异常,这个异常需要在全局异常捕获器中进行捕获。如果权限认证成功,就会进入到controller。

  • 全局异常捕获
package com.huafeng.cams.common.exception;

import com.huafeng.cams.common.R;
import io.netty.handler.codec.PrematureChannelClosureException;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;

/**
 * Author      JinXing     _让世界看到我
 * On          2020/4/23
 * Note        要采用统一返回格式,所以要处理全局的异常,统一返回.利用RestControllerAdvice实现
 */
@RestControllerAdvice
public class ExceptionController  {

    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     *
     * token认证失败的异常
     * @return
     */
    @ExceptionHandler(AuthenticationException.class)
    public R handleAuthenticationException(Exception e){
        logger.warn("身份认证失败,请重新登录!"+e.getMessage());
        return R.RESULT_ERROR("身份认证失败,请重新登录!"+e.getMessage());
    }

    /**
     *
     * 权限认证失败的异常
     * @return
     */
    @ExceptionHandler(AuthorizationException.class)
    public R handleAuthorizationException(Exception e){
        logger.warn("权限认证失败!"+e.getMessage());
        return R.RESULT_ERROR("你没有权限进行此操作!"+e.getMessage());
    }

    /**
     * 捕获其它异常
     * @param request
     * @param e
     * @return
     */
    @ExceptionHandler(Exception.class)
    public R globalException(HttpServletRequest request,Exception e){
        e.printStackTrace();
        logger.warn("操作失败!"+getStatus(request)+":"+e.getMessage());
        return R.RESULT_ERROR("操作失败!"+getStatus(request)+"-->"+e.getMessage());
    }

    /**
     * 获取request的状态码
     * @param request
     * @return
     */
    private HttpStatus getStatus(HttpServletRequest request){
        Integer code = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (code==null){
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(code);
    }
}

当权限认证失败时shiro会抛出异常, 在全局异常捕获中使用@RestControllerAdvice+@ExceptionHandler(AuthorizationException.class)可以捕获这个异常,在这里对客户端进行统一返回,当然这个全局异常捕获不仅仅捕获shiro的异常,其它的异常可以在这里捕获,采用统一的数据格式返回客户端。

  • token刷新

在使用token进行用户身份认证时,用户登录成功后会给用户签发一个token,并且每次访问资源都要携带这个token。而在签发token时,会给token标记一个过期时间,表明这个token可以用,但是不能一直用,当token过期后就没有用了,所以在用户使用时要对这个token进行刷新,及时的给用户一个可以用的新token。
笔者使用的机制是当用户在访问资源时会对其身份进行认证,如果认证成功就给用户重新签发一个token,放在response中返回给客户端。客户端在每次访问完成后在response中获取到token替换掉之前的token,然后下次访问时就用这次新签发的token。这样算token过期时间就不是从用户登录成功第一次签发的时候算,而是从此次完成访问的时候开始算。这个其实在上文中贴出filter代码块中就有所体现:

@Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        logger.info("JwtFilter.onAccessDenied");
        String token = getRequestToken((HttpServletRequest) request);
        if (StringUtils.isBlank(token)) {
            String result = new Gson().toJson(R.RESULT_ERROR("请先登录"));
            response(response,result);
            return false;
        }
        boolean login = executeLogin(request, response);
        if (login) {
            refreshToken(request, response);
        }
        return login;
    }

    /**
     * 身份信息认证成功后刷新token给客户端
     * 自定义的token刷新策略
     *
     * @param request
     * @param response
     */
    private void refreshToken(ServletRequest request, ServletResponse response) throws MyException {
        AuthenticationToken authenticationToken = createToken(request, response);
        String token = (String) authenticationToken.getCredentials();
        String newToken = JwtUtil.refreshToken(token);
        if (!StringUtils.isEmpty(newToken)){
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader(JwtUtil.TOKEN_TAG,newToken);
        }
    }

在身份认证成功后重新签发一个token放在response中返回给客户端。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值