Dalam projek asal, kerana tiada konfigurasi cache, adalah perlu untuk mengesahkan sama ada subjek semasa mempunyai hak capaian setiap kali Setiap kali, pangkalan data akan ditanya. Memandangkan data kebenaran biasanya data dengan lebih banyak bacaan dan kurang penulisan, kami harus menambah sokongan caching padanya.
Apabila kami menambah cache, Shiro akan menanyakan cache terlebih dahulu untuk data yang berkaitan apabila melakukan pengesahan Jika ia tiada dalam cache, ia akan menanyakan pangkalan data dan menulis data yang ditemui ke dalam cache pada kali berikutnya ia diperiksa, ia boleh diambil daripada cache Dapatkan data daripada cache dan bukannya daripada pangkalan data. Ini akan meningkatkan prestasi aplikasi kami.
Seterusnya, mari kita laksanakan bahagian pengurusan cache shiro.
Shiro menyediakan fungsi pengurusan sesi peringkat perusahaan yang lengkap, tidak bergantung pada bekas asas (seperti bekas web tomcat), boleh digunakan dalam kedua-dua persekitaran JavaSE dan JavaEE, dan menyediakan Pengurusan sesi, pemantauan acara sesi, storan/kegigihan sesi, pengelompokan bebas kontena, sokongan pembatalan/tamat tempoh, sokongan telus untuk Web, sokongan log masuk tunggal SSO dan ciri lain.
Kami akan menggunakan pengurusan sesi Shiro untuk mengambil alih sesi web aplikasi kami dan menyimpan maklumat sesi melalui Redis.
Dalam Shiro, ia menyediakan kelas CacheManager untuk pengurusan cache.
Dalam shiro, rangka kerja cache EhCache digunakan secara lalai. EhCache ialah rangka kerja caching dalam proses Java tulen yang pantas dan ramping.
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.4.0</version> </dependency>
Dalam proses penyepaduan Redis dengan SpringBoot, anda juga mesti memberi perhatian kepada isu padanan versi, jika tidak kaedah yang tidak dijumpai pengecualian mungkin dilaporkan.
private void enableCache(MySQLRealm realm){ //开启全局缓存配置 realm.setCachingEnabled(true); //开启认证缓存配置 realm.setAuthenticationCachingEnabled(true); //开启授权缓存配置 realm.setAuthorizationCachingEnabled(true); //为了方便操作,我们给缓存起个名字 realm.setAuthenticationCacheName("authcCache"); realm.setAuthorizationCacheName("authzCache"); //注入缓存实现 realm.setCacheManager(new EhCacheManager()); }
Kemudian panggil kaedah ini dalam getRealm.
Petua: Dalam pelaksanaan ini, hanya caching tempatan dilaksanakan. Dalam erti kata lain, data cache berkongsi memori mesin yang sama seperti aplikasi. Jika pelayan terputus atau mengalami gangguan kuasa yang tidak dijangka, data cache tidak akan wujud lagi. Sudah tentu, anda juga boleh memberikan cache lebih banyak konfigurasi melalui kaedah cacheManager.setCacheManagerConfigFile().
Seterusnya kami akan cache data kebenaran kami melalui 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>
Dalam Tambah konfigurasi berkaitan redis dalam 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
Ubah suai kelas konfigurasi ShiroConfig, tambah konfigurasi pemalam 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<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; } }
Ubah suai kaedah MySQLRealm
dalam doGetAuthenticationInfo
dan tukar User
objek Keseluruhan digunakan sebagai parameter pertama SimpleAuthenticationInfo
. shiro-redis akan mendapat nilai id daripada parameter pertama sebagai sebahagian daripada kunci data dalam redis berdasarkan nilai atribut RedisCacheManager
principalIdFieldName
.
/** * 认证 * @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; }
dan ubah suai kaedah MySQLRealm
dalam doGetAuthorizationInfo
untuk mendapatkan maklumat identiti utama daripada objek Pengguna.
/** * 授权 * @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; }
Memandangkan SimpleByteSource
lalai dalam Shiro tidak melaksanakan antara muka bersiri, garam yang dijana oleh ByteSource.Util.bytes() menyebabkan ralat semasa bersiri, jadi ia perlu disesuaikan Tentukan kelas Garam dan laksanakan antara muka bersiri. Dan gunakan new CurrentSalt(user.getSalt())
untuk menghantar nilai garam dalam kaedah pengesahan Realm tersuai.
/**由于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); } }
/**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); } }
/** * <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; } } }
Tambahkan konfigurasi pengurus sesi dalam 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; }
Dalam versi terkini (1.6.0), konfigurasi setSessionIdUrlRewritingEnabled(false) pengurus sesi tidak berkuat kuasa, menyebabkan tiada pengesahan secara langsung Ralat ubah hala berbilang berlaku apabila mengakses sumber yang dilindungi. Pepijat ini telah diselesaikan selepas menukar versi shiro kepada 1.5.0.
Asalnya artikel ini sepatutnya disiarkan semalam. . .
Sebelum maklumat pengesahan dikembalikan, kami perlu membuat pertimbangan: jika pengguna semasa telah log masuk pada peranti lama, sesi di peranti lama perlu Padam id dan jadikannya di luar talian.
/** * 认证 * @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; }
Kami menyimpan maklumat sesi dalam redis dan mengembalikan ID sesi kepada pengguna dalam bentuk token selepas pengguna disahkan. Pengguna membawa token ini apabila meminta sumber yang dilindungi Kami memperoleh maklumat kebenaran pengguna daripada redis berdasarkan maklumat token untuk melaksanakan kawalan akses.
@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 "未授权!请联系管理员授权"; } }
Dalam pembangunan sebenar, hasil yang dikembalikan harus disatukan dan kod ralat perniagaan diberikan. Ini di luar skop artikel ini Jika perlu, sila pertimbangkan berdasarkan ciri-ciri sistem anda sendiri.
Log masuk yang berjaya
Nama pengguna atau kata laluan salah
Atas sebab keselamatan, jangan dedahkan sama ada nama pengguna atau kata laluan itu salah.
Akses sumber yang dibenarkan selepas pengesahan
Akses selepas pengesahan Sumber yang tidak dibenarkan
Akses terus tanpa pengesahan
masing-masing sepadan dengan cache maklumat pengesahan, cache maklumat kebenaran dan cache maklumat sesi.
Atas ialah kandungan terperinci Bagaimana Springboot melaksanakan pengesahan dan pengurusan kebenaran dinamik. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!