0

0

Springboot如何实现认证和动态权限管理

WBOY

WBOY

发布时间:2023-06-03 21:46:06

|

1946人浏览过

|

来源于亿速云

转载

知识点补充

Shiro缓存

流程分析

在原来的项目当中,由于没有配置缓存,因此每次需要验证当前主体有没有访问权限时,都会去查询数据库。由于权限数据是典型的读多写少的数据,因此,我们应该要对其加入缓存的支持。

当我们加入缓存后,shiro在做鉴权时先去缓存里查询相关数据,缓存里没有,则查询数据库并将查到的数据写入缓存,下次再查时就能从缓存当中获取数据,而不是从数据库中获取。这样就能改善我们的应用的性能。

接下来,我们去实现shiro的缓存管理部分。

Shiro会话机制

Shiro 提供了完整的企业级会话管理功能,不依赖于底层容器(如 web 容器 tomcat),不管 JavaSE 还是 JavaEE 环境都可以使用,提供了会话管理、会话事件监听、会话存储 / 持久化、容器无关的集群、失效 / 过期支持、对 Web 的透明支持、SSO 单点登录的支持等特性。

我们将使用 Shiro 的会话管理来接管我们应用的web会话,并通过Redis来存储会话信息。

整合步骤

添加缓存

CacheManager

在Shiro当中,它提供了CacheManager这个类来做缓存管理。

使用Shiro默认的EhCache实现

在shiro当中,默认使用的是EhCache缓存框架。EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点。

引入shiro-EhCache依赖

    org.apache.shiro
    shiro-ehcache
    1.4.0

在SpringBoot整合Redis的过程中,还要注意版本匹配的问题,不然有可能报方法未找到的异常。

在ShiroConfig中添加缓存配置
private void enableCache(MySQLRealm realm){
    //开启全局缓存配置
    realm.setCachingEnabled(true);
    //开启认证缓存配置
    realm.setAuthenticationCachingEnabled(true);
    //开启授权缓存配置
    realm.setAuthorizationCachingEnabled(true);

    //为了方便操作,我们给缓存起个名字
    realm.setAuthenticationCacheName("authcCache");
    realm.setAuthorizationCacheName("authzCache");
    //注入缓存实现
    realm.setCacheManager(new EhCacheManager());
}

然后再在getRealm中调用这个方法即可。

提示:在这个实现当中,只是实现了本地的缓存。也就是说缓存的数据同应用一样共用一台机器的内存。如果服务器发生宕机或意外停电,那么缓存数据也将不复存在。当然你也可通过cacheManager.setCacheManagerConfigFile()方法给予缓存更多的配置。

接下来我们将通过Redis缓存我们的权限数据

使用Redis实现

添加依赖

        
            org.crazycake
            shiro-redis
            3.1.0
            
            
                
                    org.apache.shiro
                    shiro-core
                
            
        
配置redis

在application.yml中添加redis的相关配置

spring:
   redis:
     host: 127.0.0.1
     port: 6379
     password: hewenping
     timeout: 3000
     jedis:
       pool:
         min-idle: 5
         max-active: 20
         max-idle: 15

修改ShiroConfig配置类,添加shiro-redis插件配置

/**shiro配置类
 * @author 赖柄沣 bingfengdev@aliyun.com
 * @version 1.0
 * @date 2020/10/6 9:11
 */
@Configuration
public class ShiroConfig {

    private static final String CACHE_KEY = "shiro:cache:";
    private static final String SESSION_KEY = "shiro:session:";
    private static final int EXPIRE = 18000;
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.timeout}")
    private int timeout;
    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.jedis.pool.min-idle}")
    private int minIdle;
    @Value("${spring.redis.jedis.pool.max-idle}")
    private int maxIdle;
    @Value("${spring.redis.jedis.pool.max-active}")
    private int maxActive;

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }


    /**
     * 创建ShiroFilter拦截器
     * @return ShiroFilterFactoryBean
     */
    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //配置不拦截路径和拦截路径,顺序不能反
        HashMap map = new HashMap<>(5);

        map.put("/authc/**","anon");
        map.put("/login.html","anon");
        map.put("/js/**","anon");
        map.put("/css/**","anon");

        map.put("/**","authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        //覆盖默认的登录url
        shiroFilterFactoryBean.setLoginUrl("/authc/unauthc");
        return shiroFilterFactoryBean;
    }

    @Bean
    public Realm getRealm(){
        //设置凭证匹配器,修改为hash凭证匹配器
        HashedCredentialsMatcher myCredentialsMatcher = new HashedCredentialsMatcher();
        //设置算法
        myCredentialsMatcher.setHashAlgorithmName("md5");
        //散列次数
        myCredentialsMatcher.setHashIterations(1024);
        MySQLRealm realm = new MySQLRealm();
        realm.setCredentialsMatcher(myCredentialsMatcher);
        //开启缓存
        realm.setCachingEnabled(true);
        realm.setAuthenticationCachingEnabled(true);
        realm.setAuthorizationCachingEnabled(true);
        return realm;
    }

    /**
     * 创建shiro web应用下的安全管理器
     * @return DefaultWebSecurityManager
     */
    @Bean
    public DefaultWebSecurityManager getSecurityManager( Realm realm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
    
        securityManager.setCacheManager(cacheManager());
        SecurityUtils.setSecurityManager(securityManager);
        return securityManager;
    }



    /**
     * 配置Redis管理器
     * @Attention 使用的是shiro-redis开源插件
     * @return
     */
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setTimeout(timeout);
        redisManager.setPassword(password);
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(maxIdle+maxActive);
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMinIdle(minIdle);
        redisManager.setJedisPoolConfig(jedisPoolConfig);
        return redisManager;
    }


    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setKeyPrefix(CACHE_KEY);
        // shiro-redis要求放在session里面的实体类必须有个id标识
        //这是组成redis中所存储数据的key的一部分
        redisCacheManager.setPrincipalIdFieldName("username");
        return redisCacheManager;
    }

}

修改MySQLRealm中的doGetAuthenticationInfo方法,将User对象整体作为SimpleAuthenticationInfo的第一个参数。shiro-redis将根据RedisCacheManagerprincipalIdFieldName属性值从第一个参数中获取id值作为redis中数据的key的一部分。

/**
 * 认证
 * @param token
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    if(token==null){
        return null;
    }
    String principal = (String) token.getPrincipal();
    User user = userService.findByUsername(principal);
    SimpleAuthenticationInfo simpleAuthenticationInfo = new MyAuthcInfo(
            //由于shiro-redis插件需要从这个属性中获取id作为redis的key
            //所有这里传的是user而不是username
            user,
            //凭证信息
            user.getPassword(),
            //加密盐值
            new CurrentSalt(user.getSalt()),
            getName());
    
    return simpleAuthenticationInfo;
}

并修改MySQLRealm中的doGetAuthorizationInfo方法,从User对象中获取主身份信息。

/**
 * 授权
 * @param principals
 * @return
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
   User user = (User) principals.getPrimaryPrincipal();
    String username = user.getUsername();
    List roleList = roleService.findByUsername(username);
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    for (Role role : roleList) {
        authorizationInfo.addRole(role.getRoleName());
    }
    List roleIdList  = new ArrayList<>();
    for (Role role : roleList) {
        roleIdList.add(role.getRoleId());
    }

    List resourceList = resourceService.findByRoleIds(roleIdList);
    for (Resource resource : resourceList) {
        authorizationInfo.addStringPermission(resource.getResourcePermissionTag());
    }
    return authorizationInfo;
}
自定义Salt

由于Shiro里面默认的SimpleByteSource没有实现序列化接口,导致ByteSource.Util.bytes()生成的salt在序列化时出错,因此需要自定义Salt类并实现序列化接口。并在自定义的Realm的认证方法使用new CurrentSalt(user.getSalt())传入盐值。

/**由于shiro当中的ByteSource没有实现序列化接口,缓存时会发生错误
 * 因此,我们需要通过自定义ByteSource的方式实现这个接口
 * @author 赖柄沣 bingfengdev@aliyun.com
 * @version 1.0
 * @date 2020/10/8 16:17
 */
public class CurrentSalt extends SimpleByteSource implements Serializable {
    public CurrentSalt(String string) {
        super(string);
    }

    public CurrentSalt(byte[] bytes) {
        super(bytes);
    }

    public CurrentSalt(char[] chars) {
        super(chars);
    }

    public CurrentSalt(ByteSource source) {
        super(source);
    }

    public CurrentSalt(File file) {
        super(file);
    }

    public CurrentSalt(InputStream stream) {
        super(stream);
    }
}

添加Shiro自定义会话

添加自定义会话ID生成器

/**SessionId生成器
 * 

@author 赖柄沣 laibingf_dev@outlook.com

*

@date 2020/8/15 15:19

*/ public class ShiroSessionIdGenerator implements SessionIdGenerator { /** *实现SessionId生成 * @param session * @return */ @Override public Serializable generateId(Session session) { Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session); return String.format("login_token_%s", sessionId); } }

添加自定义会话管理器

/**
 * 

@author 赖柄沣 laibingf_dev@outlook.com

*

@date 2020/8/15 15:40

*/ public class ShiroSessionManager extends DefaultWebSessionManager { //定义常量 private static final String AUTHORIZATION = "Authorization"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; //重写构造器 public ShiroSessionManager() { super(); this.setDeleteInvalidSessions(true); } /** * 重写方法实现从请求头获取Token便于接口统一 * * 每次请求进来, * Shiro会去从请求头找Authorization这个key对应的Value(Token) * @param request * @param response * @return */ @Override public Serializable getSessionId(ServletRequest request, ServletResponse response) { String token = WebUtils.toHttp(request).getHeader(AUTHORIZATION); //如果请求头中存在token 则从请求头中获取token if (!StringUtils.isEmpty(token)) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return token; } else { // 这里禁用掉Cookie获取方式 return null; } } }

配置自定义会话管理器

在ShiroConfig中添加对会话管理器的配置

Shoping购物网源码
Shoping购物网源码

该系统采用多层模式开发,这个网站主要展示女装的经营,更易于网站的扩展和后期的维护,同时也根据常用的SQL注入手段做出相应的防御以提高网站的安全性,本网站实现了购物车,产品订单管理,产品展示,等等,后台实现了动态权限的管理,客户管理,订单管理以及商品管理等等,前台页面设计精致,后台便于操作等。实现了无限子类的添加,实现了动态权限的管理,支持一下一个人做的辛苦

下载
/**
 * SessionID生成器
 *
 */
@Bean
public ShiroSessionIdGenerator sessionIdGenerator(){
    return new ShiroSessionIdGenerator();
}

/**
 * 配置RedisSessionDAO
 */
@Bean
public RedisSessionDAO redisSessionDAO() {
    RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
    redisSessionDAO.setRedisManager(redisManager());
    redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
    redisSessionDAO.setKeyPrefix(SESSION_KEY);
    redisSessionDAO.setExpire(EXPIRE);
    return redisSessionDAO;
}

/**
 * 配置Session管理器
 * @Author Sans
 *
 */
@Bean
public SessionManager sessionManager() {
    ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
    shiroSessionManager.setSessionDAO(redisSessionDAO());
    //禁用cookie
    shiroSessionManager.setSessionIdCookieEnabled(false);
    //禁用会话id重写
    shiroSessionManager.setSessionIdUrlRewritingEnabled(false);
    return shiroSessionManager;
}

目前最新版本(1.6.0)中,session管理器的setSessionIdUrlRewritingEnabled(false)配置没有生效,导致没有认证直接访问受保护资源出现多次重定向的错误。将shiro版本切换为1.5.0后就解决了这个bug。

本来这篇文章应该是昨晚发的,因为这个原因搞了好久,所有今天才发。。。

修改自定义Realm的doGetAuthenticationInfo认证方法

在认证信息返回前,我们需要做一个判断:如果当前用户已在旧设备上登录,则需要将旧设备上的会话id删掉,使其下线。

/**
 * 认证
 * @param token
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    if(token==null){
        return null;
    }
    String principal = (String) token.getPrincipal();
    User user = userService.findByUsername(principal);
    SimpleAuthenticationInfo simpleAuthenticationInfo = new MyAuthcInfo(
            //由于shiro-redis插件需要从这个属性中获取id作为redis的key
            //所有这里传的是user而不是username
            user,
            //凭证信息
            user.getPassword(),
            //加密盐值
            new CurrentSalt(user.getSalt()),
            getName());

    //清除当前主体旧的会话,相当于你在新电脑上登录系统,把你之前在旧电脑上登录的会话挤下去
    ShiroUtils.deleteCache(user.getUsername(),true);
    return simpleAuthenticationInfo;
}

修改login接口

我们将会话信息存储在redis中,并在用户认证通过后将会话Id以token的形式返回给用户。用户请求受保护资源时带上这个token,我们根据token信息去redis中获取用户的权限信息,从而做访问控制。

@PostMapping("/login")
public HashMap login(@RequestBody LoginVO loginVO) throws AuthenticationException {
    boolean flags = authcService.login(loginVO);
    HashMap map = new HashMap<>(3);
    if (flags){
        Serializable id = SecurityUtils.getSubject().getSession().getId();
        map.put("msg","登录成功");
        map.put("token",id);
        return map;
    }else {
        return null;
    } 
}

添加全局异常处理

/**shiro异常处理
 * @author 赖柄沣 bingfengdev@aliyun.com
 * @version 1.0
 * @date 2020/10/7 18:01
 */
@ControllerAdvice(basePackages = "pers.lbf.springbootshiro")
public class AuthExceptionHandler {

    //==================认证异常====================//

    @ExceptionHandler(ExpiredCredentialsException.class)
    @ResponseBody
    public String expiredCredentialsExceptionHandlerMethod(ExpiredCredentialsException e) {
        return "凭证已过期";
    }

    @ExceptionHandler(IncorrectCredentialsException.class)
    @ResponseBody
    public String incorrectCredentialsExceptionHandlerMethod(IncorrectCredentialsException e) {
        return "用户名或密码错误";
    }

    @ExceptionHandler(UnknownAccountException.class)
    @ResponseBody
    public String unknownAccountExceptionHandlerMethod(IncorrectCredentialsException e) {
        return "用户名或密码错误";
    }

    
    @ExceptionHandler(LockedAccountException.class)
    @ResponseBody
    public String lockedAccountExceptionHandlerMethod(IncorrectCredentialsException e) {
        return "账户被锁定";
    }

    //=================授权异常=====================//

    @ExceptionHandler(UnauthorizedException.class)
    @ResponseBody
    public String unauthorizedExceptionHandlerMethod(UnauthorizedException e){
        return "未授权!请联系管理员授权";
    }
}

实际开发中,应该对返回结果统一化,并给出业务错误码。这已经超出了本文的范畴,如有需要,请根据自身系统特点考量。

进行测试

认证

登录成功的情况

Springboot如何实现认证和动态权限管理

用户名或密码错误的情况

Springboot如何实现认证和动态权限管理

为了安全起见,不要暴露具体是用户名错误还是密码错误。

访问受保护资源

认证后访问有权限的资源

Springboot如何实现认证和动态权限管理

认证后访问无权限的资源

Springboot如何实现认证和动态权限管理

未认证直接访问的情况

Springboot如何实现认证和动态权限管理

查看redis

Springboot如何实现认证和动态权限管理

三个键值分别对应认证信息缓存、授权信息缓存和会话信息缓存。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

804

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

723

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

727

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

395

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

398

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

445

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

428

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16861

2023.08.03

视频文件格式
视频文件格式

本专题整合了视频文件格式相关内容,阅读专题下面的文章了解更多详细内容。

2

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Redis6入门到精通超详细教程
Redis6入门到精通超详细教程

共47课时 | 5.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号