Shiro-JWT SpringBoot前后端分离权限认证的一种思路

JWT-Shiro 整合

JWT-与Shiro整合进行授权认证的大致思路 图示

大致思路

  1. 将登录验证从shiro中分离,自己结合JWT实现
  2. 用户登陆后请求认证服务器进行密码等身份信息确认,确认成功后 封装相关用户信息 生成token 相应给前端.
  3. 之后每次访问资源接口都在请求头中携带认证时生成的token
  4. 当发起资源请求时首先请求被请求过滤器拦截,拦截后判断请求头中是否含有token
  5. 如果含有token对token进行认证认证成功后对token进行解析,之后进行授权,拥有权限则进行放行
  6. 反之返回相关错误信息

核心点

  • token相关工具类的封装
  • 自定义重写shiro过滤器 extends AccessControlFilter
  • 自定义实现shiro Realm extends AuthorizingRealm
  • 实现自定义的shiroToken implements AuthenticationToken

具体代码

生成token的工具类

public class JWTUtils {
    //密钥用于生成token的签名
    private static final String SIGN = "!1qaz.(";

    /**
     * 生成token
     */
    public static String getToken(String userId,String userName, String roles, String permissions) {
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE, 7);
        JWTCreator.Builder builder = JWT.create()
                .withIssuer("HuangShen")//token签发者
                .withExpiresAt(instance.getTime()) //过期时间
                .withClaim("userId", userId)//相关信息
                .withClaim("userName", userName)
                .withClaim("roles", roles)
                .withClaim("permissions", permissions);

        //使用HMAC256算法生成token
        String token = builder.sign(Algorithm.HMAC256(SIGN)); 
        return token;
    }

    /**
     * 解码token
     *
     * @param token token 
     */
    public static DecodedJWT verify(String token){
        DecodedJWT verify =JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
        return verify;
    }

自定义重写shiro过滤器

public class JWTFilter extends AccessControlFilter {

    /**
     * 此方法首先执行当此方法返回false时继续执行onAccessDenied方法
     * 返回true允许访问
     * 返回 false拒绝访问
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
           
        //获取主体对象
        Subject subject = SecurityUtils.getSubject();
        System.out.println("===允许访问===");
        //当主体对象不为空且已经获得认证时允许访问 
        if (null != subject && subject.isAuthenticated()){
            return true;
        }
            return false;
    }


    /**
     * 当isAccessAllowed返回值为false时执行
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String token = httpServletRequest.getHeader("token");
        //客户端没有携带token
        if (StringUtils.isEmpty(token)) {
            System.out.println("请求头没有token");
            return true;
        }
        System.out.println("拒接访问");
        JWTToken jwtToken = new JWTToken(token);
        Subject subject = SecurityUtils.getSubject();
        //进行认证
        subject.login(jwtToken);
        return true;
    }

自定义实现shiro Realm

/**
 * 继承AuthorizingRealm类重写doGetAuthorizationInfo(授权)
 * doGetAuthenticationInfo(认证)
 */
public class CustomerRealm extends AuthorizingRealm {

    /**
     * 认证
     * @param authenticationToken  认证token
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获取主体信息
        JWTToken principal = (JWTToken) authenticationToken;
        DecodedJWT verify;
        //创建自定义principal并赋值
        TokenPayload tokenPayload = new TokenPayload();
        //解析token
        try {
            verify = JWTUtils.verify((String) principal.getPrincipal());
            tokenPayload.setUserId(verify.getClaim("userId").asString());
            tokenPayload.setRoles(verify.getClaim("roles").asString());
            tokenPayload.setUserName(verify.getClaim("userName").asString());
            tokenPayload.setPermissions(verify.getClaim("permissions").asString());
        } catch (AlgorithmMismatchException exception) {
            throw new AuthenticationException("算法不匹配异常" + exception.getMessage());
        } catch (SignatureVerificationException exception) {
            throw new AuthenticationException("签名验证异常" + exception.getMessage());
        } catch (TokenExpiredException exception) {
            throw new AuthenticationException("token过期异常" + exception.getMessage());
        } catch (InvalidClaimException exception) {
            throw new AuthenticationException("无效Claim异常" + exception.getMessage());
        } catch (JWTDecodeException exception) {
            throw new AuthenticationException("JWT解码异常" + exception.getMessage());
        } catch (JWTVerificationException exception) {
            throw new AuthenticationException("JWT验证异常" + exception.getMessage());
        } catch (RuntimeException exception) {
            throw new RuntimeException(exception.getMessage());
        }


        System.out.println("认证完成");
        //将token解析过后的信息封装成为主体传入 授权时使用
        return new SimpleAuthenticationInfo(tokenPayload, true, this.getName());
    }

    /**
     * 授权
     * @param principalCollection 授权主体
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("开始授权");
        TokenPayload primaryPrincipal = (TokenPayload) principalCollection.getPrimaryPrincipal();

        System.out.println(primaryPrincipal.getRoles());
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //添加角色信息
        simpleAuthorizationInfo.addRole(primaryPrincipal.getRoles());
        //添加权限信息
        simpleAuthorizationInfo.addStringPermission(primaryPrincipal.getPermissions());


        System.out.println("授权完成");
        return simpleAuthorizationInfo;
    }

    @Override
    public Class<?> getAuthenticationTokenClass() {
        return JWTToken.class;
    }

实现自定义的shiroToken

/**
 * 为了便于使用由JWT生成的token 自定义实现自己的token
 */
public class JWTToken implements AuthenticationToken {

    //存储由请求头中获取的token
    private final String jwtToken;

    public JWTToken(String jwtToken) {
        this.jwtToken = jwtToken;
    }

    @Override
    public Object getPrincipal() {
        return this.jwtToken;
    }

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

shiro配置

@Configuration
public class ShiroConfig {

    /**
     * 1.创建shiroFilterFactoryBean
     * 负责拦截多有请求
     *
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //给filter设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
        filters.put("jwtfilter",new JWTFilter());
        //将自定义过滤器加入到shiro 过滤其中
        shiroFilterFactoryBean.setFilters(filters);

        // 完全无状态认证  noSessionCreation 不保留每次会话的session
        //因此每次请求都会进行授权和认证
        HashMap<String, String> map = new HashMap<>();
        map.put("/shiro/login","anon");
        map.put("/shiro/**","noSessionCreation,jwtfilter");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        shiroFilterFactoryBean.setLoginUrl("/shiro/unauthorized");
        return shiroFilterFactoryBean;
    }

    /**
     * 2.创建安全管理器
     *
     * @param realm
     * @return
     */
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("myRealm") Realm realm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }

    /**
     * 3.自定义Realm
     *
     * CredentialsMatcher 证书匹配器
     * @return 自定义Realm
     */
    @Bean("myRealm")
    public Realm getRealm() {
        CustomerRealm customerRealm = new CustomerRealm();
        
        
        return customerRealm;
    }

总结

使用JWTToken与shiro进无状态授权认证时 实际上登录时放弃的使用shiro的认证 登陆时使用自己实现的登录方法并且生成Token,无状态会话,用于shiro不存储每次会话的session 因此每次请求都会进行一次完整的shiro授权认证流程 ,可以使用Redis 等其他缓存的方式实现shiro的缓存 减小系统压力。

原文地址:https://www.cnblogs.com/huangshen/p/13658912.html