Home >Java >javaTutorial >How Springboot implements authentication and dynamic permission management

How Springboot implements authentication and dynamic permission management

WBOY
WBOYforward
2023-06-03 21:46:061871browse

Knowledge point supplement

Shiro cache

Process analysis

In the original project, since there is no cache configured, it is necessary to verify whether the current subject has access rights each time Every time, the database will be queried. Since permission data is typically data with more reading and less writing, we should add caching support to it.

When we add the cache, Shiro will first query the cache for relevant data when doing authentication. If there is no data in the cache, it will query the database and write the found data to the cache. The next time it is checked, it can be retrieved from the cache. Get the data from the cache instead of from the database. This will improve the performance of our application.

Next, let’s implement the cache management part of shiro.

Shiro session mechanism

Shiro provides a complete enterprise-level session management function, which does not depend on the underlying container (such as web container tomcat). It can be used in both JavaSE and JavaEE environments and provides session Management, session event monitoring, session storage/persistence, container-independent clustering, invalidation/expiration support, transparent support for the Web, SSO single sign-on support and other features.

We will use Shiro's session management to take over our application's web session and store session information through Redis.

Integration steps

Add cache

CacheManager

In Shiro, it provides the CacheManager class for cache management.

Use Shiro's default EhCache implementation

In shiro, the EhCache cache framework is used by default. EhCache is a pure Java in-process caching framework that is fast and lean.

Introducing shiro-EhCache dependency
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.0</version>
</dependency>

In the process of integrating Redis with SpringBoot, we must also pay attention to the issue of version matching, otherwise an exception of method not found may be reported.

Add cache configuration in 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());
}

Then call this method in getRealm.

Tips: In this implementation, only local caching is implemented. In other words, the cached data shares the same machine memory as the application. If the server goes down or has an unexpected power outage, the cached data will no longer exist. Of course, you can also give the cache more configurations through the cacheManager.setCacheManagerConfigFile() method.

Next we will cache our permission data through Redis

Use Redis to implement

Add dependencies
<!--shiro-redis相关依赖-->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.1.0</version>
            <!--    里面这个shiro-core版本较低,会引发一个异常
					ClassNotFoundException: org.apache.shiro.event.EventBus
                    需要排除,直接使用上面的shiro
                    shiro1.3 加入了时间总线。-->
            <exclusions>
                <exclusion>
                    <groupId>org.apache.shiro</groupId>
                    <artifactId>shiro-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
Configure redis

In Add redis-related configuration to application.yml

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

Modify the ShiroConfig configuration class and add the shiro-redis plug-in configuration

/**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<String, String> 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;
    }

}

Modify doGetAuthenticationInfo## in MySQLRealm #Method, use the entire User object as the first parameter of SimpleAuthenticationInfo. shiro-redis will obtain the id value from the first parameter as part of the key of the data in redis based on the principalIdFieldName attribute value of RedisCacheManager.

/**
 * 认证
 * @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;
}

And modify the

doGetAuthorizationInfo method in MySQLRealm to obtain the primary identity information from the User object.

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

    List<Resource> resourceList = resourceService.findByRoleIds(roleIdList);
    for (Resource resource : resourceList) {
        authorizationInfo.addStringPermission(resource.getResourcePermissionTag());
    }
    return authorizationInfo;
}

Customized Salt
Because the default

SimpleByteSource in Shiro does not implement the serialization interface, the salt generated by ByteSource.Util.bytes() causes an error during serialization , so you need to customize the Salt class and implement the serialization interface. And use new CurrentSalt(user.getSalt()) to pass in the salt value in the custom Realm authentication method.

/**由于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);
    }
}

Add Shiro custom session

Add custom session ID generator

/**SessionId生成器
 * <p>@author 赖柄沣 laibingf_dev@outlook.com</p>
 * <p>@date 2020/8/15 15:19</p>
 */
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);
    }
}

Add custom session manager

/**
 * <p>@author 赖柄沣 laibingf_dev@outlook.com</p>
 * <p>@date 2020/8/15 15:40</p>
 */
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;
        }
    }
}

Configure custom session management

Add the configuration of the session manager in ShiroConfig

/**
 * 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;
}

In the latest version (1.6.0), the setSessionIdUrlRewritingEnabled(false) configuration of the session manager does not take effect, resulting in no authentication directly Multiple redirect errors occurred when accessing protected resources. This bug was solved after switching the shiro version to 1.5.0.

Originally, this article was supposed to be posted last night. Because of this reason, it took me a long time to post it today. . .

Modify the doGetAuthenticationInfo authentication method of custom Realm

Before the authentication information is returned, we need to make a judgment: If the current user has logged in on the old device, the session on the old device needs to be Delete the id and make it offline.

/**
 * 认证
 * @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;
}

Modify the login interface

We store the session information in redis, and return the session ID to the user in the form of a token after the user is authenticated. The user brings this token when requesting protected resources. We obtain the user's permission information from redis based on the token information to perform access control.

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

Add global exception handling

/**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 "未授权!请联系管理员授权";
    }
}

In actual development, the returned results should be unified and business error codes should be given. This is beyond the scope of this article. If necessary, please consider it based on the characteristics of your own system.

Test

Authentication

Successful login

How Springboot implements authentication and dynamic permission management

Incorrect username or password

How Springboot implements authentication and dynamic permission management

For the sake of security, do not reveal whether the username or password is wrong.

Access protected resources

Access authorized resources after authentication

How Springboot implements authentication and dynamic permission management

Access after authentication Unauthorized resources

How Springboot implements authentication and dynamic permission management

Direct access without authentication

How Springboot implements authentication and dynamic permission management

View The three key values ​​of redis

How Springboot implements authentication and dynamic permission management

respectively correspond to the authentication information cache, authorization information cache and session information cache.

The above is the detailed content of How Springboot implements authentication and dynamic permission management. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:yisu.com. If there is any infringement, please contact admin@php.cn delete