SpringBoot整合Shiro+JWT实现认证及权限校验

Posted 花伤情犹在

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot整合Shiro+JWT实现认证及权限校验相关的知识,希望对你有一定的参考价值。

序言

本文讲解如何使用SpringBoot整合Shiro框架来实现认证及权限校验,但如今的互联网已经成为前后端分离的时代,所以本文在使用SpringBoot整合Shiro框架的时候会联合JWT一起搭配使用。

Shiro

Shiroapache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份
认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。

Shiro架构图

Shiro 核心组件

用户、角色、权限之间的关系

  • 用户拥有不同角色
  • 角色拥有不同权限

1、UsernamePasswordTokenShiro 用来封装用户登录信息,使用用户的登录信息来创建令牌 Token
2、SecurityManagerShiro 的核心部分,负责安全认证和授权。
3、SujectShiro 的一个抽象概念,包含了用户信息。
4、Realm,开发者自定义的模块,根据项目的需求,验证和授权的逻辑全部写在 Realm 中。
5、AuthenticationInfo,用户的角色信息集合,认证时使用。
6、AuthorzationInfo,角色的权限信息集合,授权时使用。
7、DefaultWebSecurityManager,安全管理器,开发者自定义的Realm 需要注入到 DefaultWebSecurityManager 进行管理才能生效。
8、ShiroFilterFactoryBean,过滤器工厂,Shiro 的基本运行机制是开发者定制规则,Shiro 去执行,具体的执行操作就是由ShiroFilterFactoryBean 创建的一个个 Filter 对象来完成。

JWT

JWT(JSON WEB TOKEN)JSON网络令牌,JWT是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON格式)。它是在Web环境下两个实体之间传输数据的一项标准。实际上传输的就是一个字符串。

JWT的构成
JWT由三部分构成:Header(头部)、Payload(载荷)和Signature(签名)。

1.Header(头) 作用:记录令牌类型、签名算法等 例如:“alg":"HS256","type","JWT

2.Payload(有效载荷)作用:携带一些用户信息 例如"userId":"1","username":"mayikt"

3.Signature(签名)作用:防止Token被篡改、确保安全性 例如 计算出来的签名,一个字符串

项目环境

  • Shiro:1.4.1
  • SpringBoot:2.5.6
  • JDK:1.8

搭建项目

pom依赖

<dependencies>
<!--    lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
<!--        aop-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
<!--        web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!--        test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
<!--        shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.4.1</version>
        </dependency>
<!--        shiro-chcache-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.4.1</version>
        </dependency>
<!--        jwt-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>
<!--        fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.15</version>
        </dependency>
    </dependencies>

JWTUtil

public class JWTUtils 

    /**
     * 过期时间
     */
    private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000;

    /**
     * 校验
     * @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 username
     * @param password
     * @return
     */
    public static String sign(String username, String password) 
        try 
            //设置过期时间:获取当前时间+过期时间(毫秒)
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            //设置签名的加密算法:HMAC256
            Algorithm algorithm = Algorithm.HMAC256(password);
            // 附带username信息
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
         catch (UnsupportedEncodingException e) 
            return null;
        
    

    /**
     * 获取用户名
     * @param token
     * @return
     */
    public static String getUsername(String token) 
        if (token == null || "".equals(token)) 
            return null;
        
        try 
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
         catch (JWTDecodeException e) 
            return null;
        
    



JWTToken

JWTToken是定义的一个Token类,继承了AuthenticationToken类,实现getPrincipalgetCredentials方法,(这两个方法本来是用于获取token中的信息,和识别token的,但JWTUtils已经为我们提供了这样的方法,所以这两个方法对于JWTToken没有意义)。用于将客户端传来的Token进行封装,便于Realm识别Token类型,进行认证和授权。

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;
    


JWTFilter过滤器

因为 JWT 的整合,我们需要⾃定义⾃⼰的过滤器 JWTFilterJWTFilter 继承了 BasicHttpAuthenticationFilter,并部分原⽅法进⾏了重写。

public class JWTFilter extends BasicHttpAuthenticationFilter 

    /**
     * Header中的Token标志
     */
    private static String LOGIN_SIGN = "Authorization";

    /**
     * 是否允许访问
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) 
        if (isLoginAttempt(request, response)) 
            try 
                executeLogin(request, response);
             catch (Exception e) 
                if (e instanceof AuthorizationException) 
                    throw new AuthorizationException("访问资源权限不足!");
                 else 
                    //token 异常 认证失败
                    throw new AuthenticationException("token 异常 认证失败");
                
            
        
        return true;
    

    /**
     * 是登录尝试
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) 
        HttpServletRequest req = (HttpServletRequest) request;
        //判断是否是登录请求
        String authorization = req.getHeader(LOGIN_SIGN);
        return authorization != null;
    

    /**
     * 执行登录
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception 
        HttpServletRequest req = (HttpServletRequest) request;
        String header = req.getHeader(LOGIN_SIGN);
        JWTToken token = new JWTToken(header);
        //提交给realm进⾏登⼊,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);

        return true;
    

自定义ShiroRealm

自定义的Realm对象,该对象继承于AuthorizingRealm,实现了Shiro具体认证和授权的方法。

  • doGetAuthenticationInfo方法用于->认证:校验帐号和密码
  • doGetAuthorizationInfo方法用于->授权:授予角色和权限

另外需要注意:
必须要重写supports方法,因为是自己定义的Tokenshiro无法识别,需要修改Realm中的supports方法,使 shiro 支持自定义Token

public class ShiroRealm extends AuthorizingRealm 

    @Autowired
    private RoleService roleService;
    @Autowired
    private MenuService menuService;
    @Autowired
    private UserService userService;

    /**
     * 因为是自己定义的Token,shiro无法识别,需要修改Realm中的supports方法,使 shiro 支持自定义token。
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) 
        return token instanceof JWTToken;
    

    /**
     * 认证:校验帐号和密码
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException 
        String token = (String) authenticationToken.getCredentials();
        //从token中获取用户名
        String username = JWTUtils.getUsername(token);
        //获取数据库中存取的用户,密码是加密后的
        User user = userService.selectByUserName(username);
        if (user != null) 
            // 密码验证
            if (!JWTUtils.verify(token, username, user.getPassword())) 
                // 密码不正确
                throw new IncorrectCredentialsException();
            
            return new SimpleAuthenticationInfo(token, token, getName());
         else 
            throw new UnknownAccountException();
        
    

    /**
     * 授权:授予角色和权限
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) 
        //获取用户名
        String userName = JWTUtils.getUsername(principals.toString());
        //根据用户名查询用户
        User user = userService.selectByUserName(userName);
        //实例化一个授权信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        if (user != null) 
            //赋予角色
            List<Role> roles = roleService.selectRoleByUserId(user.getId());
            for (Role role : roles) 
                //将角色添加到授权信息中
                info.addRole(role.getRoleKey());
            
            //赋予资源
            List<Menu> permissions = menuService.selectPermsByUserId(user.getId());
            for (Menu permission : permissions) 
                //将权限添加授权信息中
                info.addStringPermission(permission.getPerms());
            
        
        return info;
    


ShiroConfig

ShiroConfig用于进行Shiro的相关配置,主要包括ShiroFilterFactoryBeanDefaultWebSecurityManagerRealm的配置。

@Configuration
public class ShiroConfig 

    /**
     * 生命周期处理器
     * @return
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() 
        return new LifecycleBeanPostProcessor();
    

    /**
     * 加密方式
     * @return
     */
    @Bean(name = "hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() 
        // 散列凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 设置哈希算法名称,这里使用MD5算法
        credentialsMatcher.setHashAlgorithmName("MD5");
        // 设置哈希迭代,这里迭代2次,相当于 md5(md5(""))
        credentialsMatcher.setHashIterations(2);
        // 设置存储的凭据16进制编码,需要和生成密码时的一样,默认是 Base64
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    

    /**
     * 自定义Realm
     * @param cacheManager
     * @return
     */
    @Bean(name = "shiroRealm")
    @DependsOn("lifecycleBeanPostProcessor")
    public ShiroRealm shiroRealm(EhCacheManager cacheManager) 
        ShiroRealm realm = new ShiroRealm();
        realm.setCacheManager(cacheManager);
        return realm;
    

    /**
     * 缓存管理器
     * @return
     */
    @Bean(name = "ehCacheManager")
    @DependsOn("lifecycleBeanPostProcessor")
    public EhCacheManager ehCacheManager() 
        EhCacheManager ehCacheManager = new EhCacheManager();
        ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
        return ehCacheManager;
    

    /**
     * 安全管理器
     * @param shiroRealm
     * @return
     */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm) 
        // 实例化会话管理器
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置缓存管理器
        securityManager.setCacheManager(ehCacheManager());

        /**
         * 关闭shiro自带的session
         * 详情见文档: http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
        evaluator.setSessionStorageEnabled(false);

        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        subjectDAO.setSessionStorageEvaluator(evaluator);

        securityManager.setSubjectDAO(subjectDAO);

        // 设置自定义Realm
        securityManager.setRealm(shiroRealm);
        return securityManager;
    

    /**
     * 过滤工厂
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) 
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("jwt", new JWTFilter());

        factoryBean.setFilters(filters);

        Map<String, String> filterChainDefinitionManager = new LinkedHashMap<>();
        // 所有请求通过我们自己的JWT Filter
        filterChainDefinitionManager.put("/**", "jwt");
        factoryBean.setFilterChainDefinitionMap(filterChainDefinitionManager);
        return factoryBean;
    

    /**
     * 自动代理配置
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() 
        DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        proxyCreator.setProxyTargetClass(true);
        return proxyCreator;
    

    /**
     * 开启注解支持
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor 以上是关于SpringBoot整合Shiro+JWT实现认证及权限校验的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot技术专题「权限校验专区」Shiro整合JWT授权和认证实现

SpringBoot技术专题「实战开发系列」带你一同探索Shiro整合JWT授权和认证实战开发

手把手教你Shiro整合JWT实现登录认证

SpringBoot+Shiro+Jwt实现登录认证——最干的干货

springboot整合shiro实现登录认证以及授权

全网最细致SpringBoot整合Spring Security + JWT实现用户认证