원본 프로젝트에서는 구성된 캐시가 없으므로 현재 주체에게 접근 권한이 있는지 확인해야 할 때마다 데이터베이스를 쿼리합니다. 권한 데이터는 일반적으로 읽기는 많고 쓰기는 적은 데이터이므로 여기에 캐싱 지원을 추가해야 합니다.
캐시를 추가하면 Shiro는 인증 시 관련 데이터를 먼저 캐시에 쿼리합니다. 캐시에 없으면 데이터베이스에 쿼리하고 찾은 데이터를 캐시에서 가져옵니다. 데이터베이스에서 가져오는 대신 다음에 검사할 때 캐시합니다. 이렇게 하면 애플리케이션의 성능이 향상됩니다.
다음으로 shiro의 캐시 관리 부분을 구현해 보겠습니다.
Shiro는 기본 컨테이너(예: 웹 컨테이너 Tomcat)에 의존하지 않는 완전한 엔터프라이즈 수준 세션 관리 기능을 제공하며 JavaSE 및 JavaEE 환경 모두에서 사용할 수 있습니다. 이벤트 모니터링 및 세션 저장 / 지속성, 컨테이너 독립적 클러스터링, 무효화/만료 지원, 웹에 대한 투명한 지원, SSO 싱글 사인온 지원 및 기타 기능.
Shiro의 세션 관리를 사용하여 애플리케이션의 웹 세션을 인계받고 Redis를 통해 세션 정보를 저장합니다.
Shiro에서는 캐시 관리를 위한 CacheManager 클래스를 제공합니다.
shiro에서는 EhCache 캐싱 프레임워크가 기본적으로 사용됩니다. EhCache는 빠르고 간결한 순수 Java 프로세스 내 캐싱 프레임워크입니다.
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.4.0</version> </dependency>
Redis를 SpringBoot와 통합하는 과정에서 버전 매칭 문제도 주의해야 합니다. 그렇지 않으면 Method not Found 예외가 보고될 수 있습니다.
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를 통해 권한 데이터를 캐시하겠습니다.
<!--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>
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; } }
MySQLRealm
의 doGetAuthenticationInfo
메서드를 수정하여 전체 User
개체를 SimpleAuthenticationInfo</code의 첫 번째 개체로 사용하세요. > 매개변수. shiro-redis는 <code>RedisCacheManager
의 principalIdFieldName
속성 값을 기반으로 redis의 데이터 키의 일부로 첫 번째 매개변수에서 id 값을 가져옵니다. MySQLRealm
中的doGetAuthenticationInfo
方法,将User
对象整体作为SimpleAuthenticationInfo
的第一个参数。shiro-redis将根据RedisCacheManager
的principalIdFieldName
属性值从第一个参数中获取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; }
由于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); } }
MySQLRealm
의 doGetAuthorizationInfo
메서드를 수정하여 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); } }
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; } } }
/** * 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; }
@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; } }
/**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가 인증 및 동적 권한 관리를 구현하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!