Springboot+jwt+shiro实现用户权限控制

目录

JWT

Shiro

实现过程

1.Springboot环境搭建

2.实现JwtToken、JwtUtil、JwtFilter、这三个类


最近在做前后分离项目,前端验证用到了JWT,后端用的shiro做权限验证,基于springboot实现JWT+Shiro鉴权。

JWT

JWT 英文名是 Json Web Token ,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。

JWT 以 JSON 对象的形式安全传递信息。因为存在数字签名,因此所传递的信息是安全的。这是一个完整的token,分别包含Header:头部,Payload:负载,Signature:签名

Shiro

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。

使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

三个核心组件:Subject,SecurityManager和Realms.

Subject代表了当前用户的安全操作,即“当前操作用户”。

SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。

Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。

ShiroBasicArchitecture

实现过程

1.Springboot环境搭建

简单的说一下实现的过程,用户登录验证通过以后会返回一个token,这个token包含了用户的基本信息,前端每次的请求中header中都会携带这个token ,后端的Shiro来验证token的有效性及权限。

SpringBoot 2.2.5.RELEASE

 <dependency>
     <groupId>org.apache.shiro</groupId>
     <artifactId>shiro-spring</artifactId>
     <version>1.5.3</version>
</dependency>
 <dependency>
     <groupId>com.auth0</groupId>
     <artifactId>java-jwt</artifactId>
     <version>3.10.3</version>
</dependency>

2.实现JwtToken、JwtUtil、JwtFilter、这三个类

JwtToken基于AuthenticationToken这个类实现,作用就是Shiro的用户验证token替换为基于JWT生成的toekn

JwtToken类

package com.tdrc.common.core.shiro;

import org.apache.shiro.authc.AuthenticationToken;

/**
 * @author dpf
 * @version 1.0
 * @date 2020-5-22 15:22
 * @instruction ...
 */
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;
    }
}

JwtUtil是生成、解析、验证token的工具类,这里就不贴代码,JWT官网有详细的文档,网上也有许多的例子。

JwtFilter是BasicHttpAuthenticationFilter实现的过滤器,过滤器中主要实现了这四个方法isLoginAttempt、executeLogin、isAccessAllowed、preHandle

isLoginAttempt,判断用户是否需要登录,这里做了token的非空验证,默认是所有的前端请求都必须携带token。

/**
     * 判断用户是否想要登入。
     * 检测header里面是否包含Authorization字段即可
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse)response;
        httpServletRequest.getHeaderNames();
        String access_token = httpServletRequest.getHeader("access-token");
        //这只判断access_token是否为空
        if(!StringUtils.isEmpty(access_token)){
            access_token = access_token.substring(7);
            // 判断token是否过期
            log.info("判断用户是否想要登录:{}",access_token);
            return true;
        }else{
            HttpServletResponseUtil.printJson(httpServletResponse,new JsonResult(ResultCode.PERMISSION_UNAUTHORISE));
            return false;

        }
    }

executeLogin,获取token并执行登录的过程,具体是交给Shiro的realm来实现登录验证

 /**
     *执行登录
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response)  {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String access_token = httpServletRequest.getHeader("access-token").substring(7);
        log.info("判断用户是否想要登录x:{}",access_token);
        JwtToken token = new JwtToken(access_token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

isAccessAllowed,验证权限,登录成功即表示拥有访问系统资源的权限,具体的资源权限会在shiro中进行鉴权。

/**
     * 这里我们详细说明下为什么最终返回的都是true,即允许访问
     * 例如我们提供一个地址 GET /article
     * 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
     * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)    {
        HttpServletResponse   httpServletResponse =(HttpServletResponse)response;
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
                HttpServletResponseUtil.printJson(httpServletResponse,new JsonResult(ResultCode.PERMISSION_TOKEN_INVALID));
                return false;
            }
        }else{
            return false;
        }

    }

preHandle提供跨域支持

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

3. 重写realm类MyRealm并创建Shiro的配置类ShiroConfig

MyRealm中主要实现了两个方法doGetAuthenticationInfo、doGetAuthorizationInfo。

doGetAuthenticationInfo验证用户身份的真实性及token的有效性

    /**
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
        System.out.println("————身份认证方法————");
        String token = (String) authenticationToken.getCredentials();
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        // 验证token的有效性,如果无效会抛出异常
        try{
            JwtTokenUtil.parseJWT(token,audience.getBase64Secret());
            if(JwtTokenUtil.isExpiration(token,audience.getBase64Secret())){
                HttpServletResponseUtil.printJson(response,new JsonResult(ResultCode.PERMISSION_TOKEN_EXPIRED));
            }
        }catch (Exception e){
            HttpServletResponseUtil.printJson(response,new JsonResult(ResultCode.PERMISSION_TOKEN_INVALID));
        }
        return new SimpleAuthenticationInfo(token, token, "my_realm");
    }

doGetAuthorizationInfo,对于需要验证的资源进行权限验证

 /**
     * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("########################权限认证############################");
        String userId = JwtTokenUtil.getUserId(principals.toString(), audience.getBase64Secret());
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
         info.addStringPermission("/user/list");
        return info;
    }

ShiroConfig主要就是一些shiro的相关配置,这里面和JWT相关的配置就是禁用session、引入JwtFilter

package com.tdrc.common.core.shiro;


import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

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

/**
 * @author dpf
 * @version 1.0
 * @date 2020-5-22 15:01
 * @instruction ...
 */

@Configuration
public class ShiroConfig {
    private Logger logger = LoggerFactory.getLogger(ShiroConfig.class);
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultSecurityManager securityManager) {
        logger.info("######################Shiro 启动#######################################");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setUnauthorizedUrl("/401");
        /**
         * 自定义url规则
         * http://shiro.apache.org/web.html#urls-
         */
        Map<String, String> filterRuleMap = new LinkedHashMap<String, String>();
        // 所有的请求通过我们自己的JWT filter
        filterRuleMap.put("/401", "anon");
        filterRuleMap.put("/404", "anon");
        filterRuleMap.put("/*.png", "anon");
        filterRuleMap.put("/user/login", "anon");
        filterRuleMap.put("/article/**", "anon");
        filterRuleMap.put("/**", "jwt");
        // 访问401和404页面不通过我们的Filter


        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return shiroFilterFactoryBean;
    }

    @Bean(name = "securityManager")
    public DefaultSecurityManager getDefaultSecurityManager(@Qualifier("myRelam") MyRealm myRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm);
        /*
         * 关闭shiro自带的session
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }

    @Bean(name="myRelam")
    public MyRealm getMyRealm() {
        return new MyRealm();
    }

    /**
     * 下面的代码是添加注解支持
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

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

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

}

最后就是JWT的token的时效性,问题就是每次登陆都会创建一个新的token,在旧的token还没有过期之前都是可以使用的,还有就是集群或者分布式应用中无法保证token的唯一性,这里可以用引入缓存来解决这个问题。

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值