>  기사  >  Java  >  Springboot가 인증 및 동적 권한 관리를 구현하는 방법

Springboot가 인증 및 동적 권한 관리를 구현하는 방법

WBOY
WBOY앞으로
2023-06-03 21:46:061737검색

보충 지식 포인트

Shiro 캐시

프로세스 분석

원본 프로젝트에서는 구성된 캐시가 없으므로 현재 주체에게 접근 권한이 있는지 확인해야 할 때마다 데이터베이스를 쿼리합니다. 권한 데이터는 일반적으로 읽기는 많고 쓰기는 적은 데이터이므로 여기에 캐싱 지원을 추가해야 합니다.

캐시를 추가하면 Shiro는 인증 시 관련 데이터를 먼저 캐시에 쿼리합니다. 캐시에 없으면 데이터베이스에 쿼리하고 찾은 데이터를 캐시에서 가져옵니다. 데이터베이스에서 가져오는 대신 다음에 검사할 때 캐시합니다. 이렇게 하면 애플리케이션의 성능이 향상됩니다.

다음으로 shiro의 캐시 관리 부분을 구현해 보겠습니다.

Shiro 세션 메커니즘

Shiro는 기본 컨테이너(예: 웹 컨테이너 Tomcat)에 의존하지 않는 완전한 엔터프라이즈 수준 세션 관리 기능을 제공하며 JavaSE 및 JavaEE 환경 모두에서 사용할 수 있습니다. 이벤트 모니터링 및 세션 저장 / 지속성, 컨테이너 독립적 클러스터링, 무효화/만료 지원, 웹에 대한 투명한 지원, SSO 싱글 사인온 지원 및 기타 기능.

Shiro의 세션 관리를 사용하여 애플리케이션의 웹 세션을 인계받고 Redis를 통해 세션 정보를 저장합니다.

통합 단계

캐시 추가

CacheManager

Shiro에서는 캐시 관리를 위한 CacheManager 클래스를 제공합니다.

Shiro의 기본 EhCache 구현 사용

shiro에서는 EhCache 캐싱 프레임워크가 기본적으로 사용됩니다. EhCache는 빠르고 간결한 순수 Java 프로세스 내 캐싱 프레임워크입니다.

shiro-EhCache 종속성 소개
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.0</version>
</dependency>

Redis를 SpringBoot와 통합하는 과정에서 버전 매칭 문제도 주의해야 합니다. 그렇지 않으면 Method not Found 예외가 보고될 수 있습니다.

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에서 이 메소드를 호출하세요.

Tips: 이 구현에서는 로컬 캐싱만 구현됩니다. 즉, 캐시된 데이터는 애플리케이션과 동일한 시스템 메모리를 공유합니다. 서버가 다운되거나 예상치 못한 정전이 발생하면 캐시된 데이터는 더 이상 존재하지 않게 됩니다. 물론, cashManager.setCacheManagerConfigFile() 메서드를 통해 캐시에 더 많은 구성을 제공할 수도 있습니다.

다음으로 Redis를 통해 권한 데이터를 캐시하겠습니다.

Redis를 사용하여 구현

종속성 추가
<!--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>
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 플러그 추가 -in 구성

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

}

MySQLRealmdoGetAuthenticationInfo 메서드를 수정하여 전체 User 개체를 SimpleAuthenticationInfo의 첫 번째 개체로 사용하세요. > 매개변수. shiro-redis는 RedisCacheManagerprincipalIdFieldName 속성 값을 기반으로 redis의 데이터 키의 일부로 첫 번째 매개변수에서 id 값을 가져옵니다. 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<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;
}
自定义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);
    }
}

그리고 MySQLRealmdoGetAuthorizationInfo 메서드를 수정하여 User 개체에서 기본 ID 정보를 가져옵니다.

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

Customized Salt

Shiro의 기본 SimpleByteSource는 직렬화 인터페이스를 구현하지 않으므로 ByteSource.Util.bytes()에서 생성된 솔트는 직렬화 중에 오류를 발생시키므로 사용자 정의 Salt 클래스는 다음과 같습니다. 필수 그리고 직렬화 인터페이스를 구현하십시오. 그리고 new CurrentSalt(user.getSalt())를 사용하여 사용자 정의 Realm 인증 방법에 솔트 값을 전달하세요.

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

Shiro 사용자 정의 세션 추가

사용자 정의 세션 ID 생성기 추가

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

사용자 정의 세션 관리자 추가

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

사용자 정의 세션 관리자 구성

ShiroConfig에서 세션 관리자 구성 추가

@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;
    } 
}

최신 버전(1.6.0)에서는, 세션 관리자의 setSessionIdUrlRewritingEnabled(false) 구성이 적용되지 않아 인증 없이 보호된 리소스에 직접 액세스할 때 여러 리디렉션 오류가 발생합니다. 이 버그는 shiro 버전을 1.5.0으로 전환한 후 해결되었습니다.

원래 이 글은 어젯밤에 올릴 예정이었는데, 이런저런 이유로 오늘 올리는데 시간이 많이 걸렸네요. . .

Custom Realm의 doGetAuthenticationInfo 인증 방법을 수정하세요

인증 정보가 반환되기 전에 판단해야 합니다. 현재 사용자가 이전 기기에 로그인한 경우 이전 기기의 세션 ID를 삭제하고 이를 은(는) 오프라인입니다.

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

로그인 인터페이스 수정

우리는 세션 정보를 redis에 저장하고, 사용자가 인증된 후 세션 ID를 토큰 형태로 사용자에게 반환합니다. 사용자는 보호된 리소스를 요청할 때 이 토큰을 가져옵니다. 우리는 토큰 정보를 기반으로 Redis에서 사용자의 권한 정보를 얻어 액세스 제어를 수행합니다.

rrreee

전역 예외 처리 추가

rrreee

실제 개발에서는 반환되는 결과가 통일되어야 하며 비즈니스 오류 코드가 제공되어야 합니다. 이는 이 글의 범위를 벗어나는 내용이므로, 필요한 경우에는 귀하의 시스템 특성에 따라 고려하시기 바랍니다.

테스트 중

인증

Springboot가 인증 및 동적 권한 관리를 구현하는 방법

로그인 성공🎜🎜🎜🎜🎜

잘못된 사용자 이름 또는 비밀번호

Springboot가 인증 및 동적 권한 관리를 구현하는 방법

보안상의 이유로 사용자 이름 또는 비밀번호가 잘못되었는지 공개하지 마세요.

보호된 리소스에 액세스

인증 후 승인된 리소스에 액세스

Springboot가 인증 및 동적 권한 관리를 구현하는 방법

인증 후 승인되지 않은 리소스에 액세스

Springboot가 인증 및 동적 권한 관리를 구현하는 방법

인증 없이 직접 액세스

Springboot가 인증 및 동적 권한 관리를 구현하는 방법

보기 세 가지 핵심 가치 redis

Springboot가 인증 및 동적 권한 관리를 구현하는 방법

각각 인증정보 캐시, 인증정보 캐시, 세션정보 캐시에 해당합니다.

위 내용은 Springboot가 인증 및 동적 권한 관리를 구현하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제