Shiro
一、什么是shiro
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
shiro主要有三个核心组件组成: Subject 、SecurityMannager 和Realms
Subject :即"当前操作用户"
SecurityMannager: shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务
Realm: Realm充当了Shiro与应用安全数据间的"桥梁"或者"连接器",也就说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户以及其权限信息
所以,Realm相当于一个安全相关的DAO,它封装了数据源的连接细节,并在需要时将相关的数据提供给shiro,当配置Shiro时,你必须至少指定一个Realm,用于认证或授权,配置多个Realm是可以的,但是至少需要一个
常用的功能:
Authentication: 身份认证/登录(账号密码验证)
Authoriztion: 授权,即角色或者权限验证 (权限)
Session Manager: 会话管理,用户登录之后Session 相关管理
Cryptography: 用于密码的加密(一般是以md5进行加密)
Web Support: 集成于Web环境
Caching: 缓存,可以将用户信息、角色、权限等缓存到Redis中,这样就不用在每次去查询数据库
首先,在ShiroConfig
配置的会ShiroFilterFactoryBean
,会过滤掉不需要验证的路径,如登录请求,在第一次登录时,会将token放入redis缓存,而需要验证的路径通过自定义的JwtFilter
取出请求Header中的token,调用getSubject(request, response).login(jwtToken)
进一步验证token,在调用login后,进入自定义CustomRealm
,调用父类AuthorizingRealm
的doGetAuthenticationInfo(AuthenticationToken authenticationToken)
方法,进行具体的token验证,验证成功后,将token中的信息取出构建SimpleAuthenticationInfo
,并返回。
如果Controller配置了@RequiresPermissions("perms")
注解,并且没有被过滤的请求,则会进入自定义CustomRealm
的doGetAuthorizationInfo(PrincipalCollection principalCollection)
,在这里通过SimpleAuthenticationInfo
中包含用户信息进而获取用户权限并,然后shiro会比对perms。但是需要注意每次合规请求都会走这里,所以需要可以在ShiroConfig中配置redis缓存用户权限,在doGetAuthorizationInfo之前会先读缓存,缓存中有就会直接跳过,但是需要注意修改权限后,需要刷新redis缓存。
二、springboot 整合shiro
pom相关jar
<!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>${shiro.spring.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>${shiro.spring.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.spring.version}</version> </dependency> <!-- shiro-redis --> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>${shiro-redis.version}</version> <exclusions> <exclusion> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> </exclusion> </exclusions> </dependency> <!-- 添加jwt的依赖 --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>${java.jwt.version}</version> </dependency>
自定义Realm
public class CustomRealm extends AuthorizingRealm { @Autowired private SysUserService sysUserService; @Autowired private RedisUtil redisUtil; /** * 必须重写此方法,不然Shiro会报错 */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } /** * 根据当前登录人来返回对应授权配置类 权限的处理 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 获取登录的用户名 JwtInfo jwtInfo = (JwtInfo) principalCollection.getPrimaryPrincipal(); // 查询用户的名称 SysUserVO sysUser = sysUserService.queryUserAuth(jwtInfo.getUsername()); // 添加角色和权限 SimpleAuthorizationInfo simpleAuthenticationInfo = new SimpleAuthorizationInfo(); for(SysRole role : sysUser.getRoles()) { // 添加角色 simpleAuthenticationInfo.addRole(role.getRoleName()); // 添加权限 for(SysPermission permission : role.getPermissions()) { simpleAuthenticationInfo.addStringPermission(permission.getPerms()); } } return simpleAuthenticationInfo; } /** * 根据表单提交上的用户名来创建出 权限配置类 用于姓名和密码的校验 * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { Object tokenObject = authenticationToken.getCredentials(); if (Objects.isNull(tokenObject)) { throw new AuthenticationException("token为空!"); } String token = tokenObject.toString(); // 校验token有效性 JwtInfo jwtInfo = null; try { jwtInfo = this.checkUserTokenIsEffect(token); } catch (AuthenticationException e) { e.printStackTrace(); return null; } return new SimpleAuthenticationInfo(jwtInfo, token, getName()); } /** * 校验token的有效性 * * @param token */ public JwtInfo checkUserTokenIsEffect(String token) throws AuthenticationException { // 解密获得username,用于和数据库进行对比 String username = JwtUtil.getUsername(token); if (username == null) { throw new AuthenticationException("token非法无效!"); } // 查询用户信息 LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SysUser::getUsername, username); SysUser sysUser = sysUserService.getOne(queryWrapper); // 判断用户状态 // 校验token是否超时失效 & 或者账号密码是否错误 JwtInfo jwtInfo = new JwtInfo(); if (!jwtTokenRefresh(token, username, sysUser.getPassword())) { throw new AuthenticationException("Token失效,请重新登录!"); } else { BeanUtils.copyProperties(sysUser, jwtInfo); } return jwtInfo; } /** * JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能) * 先验证jwt中token是否有效,无效,再验证redis,中token作为key是否存在,如果无效则证明过期 * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍 * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证 * 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算 * 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。 * 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。 * 用户过期时间 = Jwt有效时间 * 2。 * * @param username * @param password * @return */ public boolean jwtTokenRefresh(String token, String username, String password) { String cacheToken = String.valueOf(redisUtil.get(RedisKeyConstant.PREFIX_USER_TOKEN + token)); if (oConvertUtils.isNotEmpty(cacheToken)) { // 校验token有效性 if (!JwtUtil.verify(cacheToken, username, password)) { String newAuthorization = JwtUtil.sign(username, password); // 设置超时时间 redisUtil.set(RedisKeyConstant.PREFIX_USER_TOKEN + token, newAuthorization); redisUtil.expire(RedisKeyConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000); } return true; } return false; } }
public class JwtToken implements AuthenticationToken { private static final long serialVersionUID = 1L; private String token; public JwtToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
public class JwtInfo implements Serializable { private String username; private String password; private String id; private String realname; public JwtInfo() { } public JwtInfo(String username, String password, String id, String realname) { this.username = username; this.password = password; this.id = id; this.realname = realname; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getRealname() { return realname; } public void setRealname(String name) { this.realname = name; } public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } JwtInfo jwtInfo = (JwtInfo) o; if (username != null ? !username.equals(jwtInfo.username) : jwtInfo.username != null) { return false; } return id != null ? id.equals(jwtInfo.id) : jwtInfo.id == null; } @Override public int hashCode() { int result = username != null ? username.hashCode() : 0; result = 31 * result + (id != null ? id.hashCode() : 0); return result; } }
创建Shiro配置文件
@Configuration public class ShiroConfig { @Resource LettuceConnectionFactory lettuceConnectionFactory; @Autowired private Environment env; @Bean public Realm realm(){ CustomRealm customerRealm = new CustomRealm(); //开启缓存管理 return customerRealm; } @Bean("securityManager") public DefaultWebSecurityManager securityManager(@Qualifier("realm") Realm realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm); /* * 关闭shiro自带的session,详情见文档 * http://shiro.apache.org/session-management.html#SessionManagement- * StatelessApplications%28Sessionless%29 */ DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO); //自定义缓存实现,使用redis securityManager.setCacheManager(redisCacheManager()); return securityManager; } /** * 该类的作用是,实现注解的方式来设置权限 */ @Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisor = new DefaultAdvisorAutoProxyCreator(); advisor.setProxyTargetClass(true); return advisor; } /** * 实现注解的方式来配置权限 */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * Filter Chain定义说明 * * 1、一个URL可以配置多个Filter,使用逗号分隔 * 2、当设置多个过滤器时,全部验证通过,才视为通过 * 3、部分过滤器可指定参数,如perms,roles */ @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 拦截器 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); // 配置不会被拦截的链接 顺序判断 filterChainDefinitionMap.put("/sys/user/login", "anon"); //登录接口排除 filterChainDefinitionMap.put("/", "anon"); filterChainDefinitionMap.put("/doc.html", "anon"); filterChainDefinitionMap.put("/**/*.js", "anon"); filterChainDefinitionMap.put("/**/*.css", "anon"); filterChainDefinitionMap.put("/**/*.html", "anon"); filterChainDefinitionMap.put("/**/*.svg", "anon"); filterChainDefinitionMap.put("/**/*.pdf", "anon"); filterChainDefinitionMap.put("/**/*.jpg", "anon"); filterChainDefinitionMap.put("/**/*.png", "anon"); filterChainDefinitionMap.put("/**/*.ico", "anon"); //remote接口排除 filterChainDefinitionMap.put("/remote/es/**", "anon"); filterChainDefinitionMap.put("/druid/**", "anon"); filterChainDefinitionMap.put("/swagger-ui.html", "anon"); filterChainDefinitionMap.put("/swagger**/**", "anon"); filterChainDefinitionMap.put("/webjars/**", "anon"); filterChainDefinitionMap.put("/v2/**", "anon"); filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon"); //性能监控 TODO 存在安全漏洞泄露TOEKN(durid连接池也有) filterChainDefinitionMap.put("/actuator/**", "anon"); // 添加自己的过滤器并且取名为jwt Map<String, Filter> filterMap = new HashMap<String, Filter>(1); // 如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】 filterMap.put("jwt", new JwtFilter(true)); shiroFilterFactoryBean.setFilters(filterMap); // <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 filterChainDefinitionMap.put("/**", "jwt"); // 未授权界面返回JSON // shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403"); // shiroFilterFactoryBean.setLoginUrl("/sys/common/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * cacheManager 缓存 redis实现 * 使用的是shiro-redis开源插件 * * @return */ public RedisCacheManager redisCacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); //redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识) redisCacheManager.setPrincipalIdFieldName("id"); //用户权限信息缓存时间 redisCacheManager.setExpire(200000); return redisCacheManager; } /** * 配置shiro redisManager * 使用的是shiro-redis开源插件 * * @return */ @Bean public IRedisManager redisManager() { IRedisManager manager; // redis 单机支持,在集群为空,或者集群无机器时候使用 if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) { RedisManager redisManager = new RedisManager(); redisManager.setHost(lettuceConnectionFactory.getHostName()); redisManager.setPort(lettuceConnectionFactory.getPort()); redisManager.setDatabase(lettuceConnectionFactory.getDatabase()); redisManager.setTimeout(0); if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) { redisManager.setPassword(lettuceConnectionFactory.getPassword()); } manager = redisManager; }else{ // redis集群支持,优先使用集群配置 RedisClusterManager redisManager = new RedisClusterManager(); Set<HostAndPort> portSet = new HashSet<>(); lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort()))); if (oConvertUtils.isNotEmpty(lettuceConnectionFactory.getPassword())) { JedisCluster jedisCluster = new JedisCluster(portSet, 2000, 2000, 5, lettuceConnectionFactory.getPassword(), new GenericObjectPoolConfig()); redisManager.setPassword(lettuceConnectionFactory.getPassword()); redisManager.setJedisCluster(jedisCluster); } else { JedisCluster jedisCluster = new JedisCluster(portSet); redisManager.setJedisCluster(jedisCluster); } manager = redisManager; } return manager; } }
@Slf4j public class CustomShiroFilterFactoryBean extends ShiroFilterFactoryBean { @Override public Class getObjectType() { return MySpringShiroFilter.class; } @Override protected AbstractShiroFilter createInstance() throws Exception { SecurityManager securityManager = getSecurityManager(); if (securityManager == null) { String msg = "SecurityManager property must be set."; throw new BeanInitializationException(msg); } if (!(securityManager instanceof WebSecurityManager)) { String msg = "The security manager does not implement the WebSecurityManager interface."; throw new BeanInitializationException(msg); } FilterChainManager manager = createFilterChainManager(); //Expose the constructed FilterChainManager by first wrapping it in a // FilterChainResolver implementation. The AbstractShiroFilter implementations // do not know about FilterChainManagers - only resolvers: PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver(); chainResolver.setFilterChainManager(manager); Map<String, Filter> filterMap = manager.getFilters(); Filter invalidRequestFilter = filterMap.get(DefaultFilter.invalidRequest.name()); if (invalidRequestFilter instanceof InvalidRequestFilter) { //此处是关键,设置false跳过URL携带中文400,servletPath中文校验bug ((InvalidRequestFilter) invalidRequestFilter).setBlockNonAscii(false); } //Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built //FilterChainResolver. It doesn't matter that the instance is an anonymous inner class //here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts //injection of the SecurityManager and FilterChainResolver: return new MySpringShiroFilter((WebSecurityManager) securityManager, chainResolver); } private static final class MySpringShiroFilter extends AbstractShiroFilter { protected MySpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) { if (webSecurityManager == null) { throw new IllegalArgumentException("WebSecurityManager property cannot be null."); } else { this.setSecurityManager(webSecurityManager); if (resolver != null) { this.setFilterChainResolver(resolver); } } } } }
@Component @Slf4j public class JwtFilter extends BasicHttpAuthenticationFilter { /** * 默认开启跨域设置(使用单体) * 微服务情况下,此属性设置为false */ private boolean allowOrigin = true; public JwtFilter(){} public JwtFilter(boolean allowOrigin){ this.allowOrigin = allowOrigin; } /** * 执行登录认证 * * @param request * @param response * @param mappedValue * @return */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { try { executeLogin(request, response); return true; } catch (Exception e) { throw new AuthenticationException("Token失效,请重新登录", e); } } /** * */ @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = httpServletRequest.getHeader(UserCommonConstant.ACCESS_TOKEN); if (StringUtils.isEmpty(token)) { token = httpServletRequest.getParameter(UserCommonConstant.ACCESS_TOKEN); } JwtToken jwtToken = new JwtToken(token); // 提交给realm进行登入,如果错误他会抛出异常并被捕获 getSubject(request, response).login(jwtToken); // 如果没有抛出异常则代表登入成功,返回true return true; } /** * 对跨域提供支持 */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; if(allowOrigin){ httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true"); } // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } }
登录接口优化
@ApiOperation("登录") @RequestMapping(value = "/login", method = RequestMethod.POST) public JSONResponse login(@RequestBody JwtAuthenticationRequest jwtAuthenticationRequest) throws Exception { String username = jwtAuthenticationRequest.getUsername(); String password = jwtAuthenticationRequest.getPassword(); //1. 验证校验码 Object sessionCode = redisUtil.get(String.format(RedisKeyConstant.REDIS_KEY_CAPTCHA, jwtAuthenticationRequest.getUuid())); if (Objects.isNull(sessionCode)) { throw new UserInvalidException("验证码过期"); } if (Objects.isNull(jwtAuthenticationRequest.getVerCode()) || !sessionCode.equals(jwtAuthenticationRequest.getVerCode().trim().toLowerCase())) { throw new UserInvalidException("验证码不正确"); } //2. 获取一个主题,将来用于封装用户的用户信息(登录成功后),该subject,shiro会自动管理 LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SysUser::getUsername, username); SysUser sysUser = sysUserService.getOne(queryWrapper); String userPassword = PasswordUtil.encrypt(username, password, sysUser.getSalt()); String sysPassword = sysUser.getPassword(); if (!sysPassword.equals(userPassword)) { if (redisUtil.incr("retryTimesKey:" + jwtAuthenticationRequest.getUsername(), 1L, 60) > 5) { //更新账户状态 return super.toJSONString(1, "账号已锁定,请联系管理员"); } else { return super.toJSONString(1, "用户名或者密码错误"); } } //3. 验证账户合法性,省略 //4. 生成token String token = JwtUtil.sign(sysUser.getUsername(), sysUser.getPassword()); redisUtil.set(RedisKeyConstant.PREFIX_USER_TOKEN + token, token); redisUtil.expire(RedisKeyConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000); Map<String, Object> result = new HashMap<>(); result.put("accessToken", token); result.put("id", sysUser.getId()); result.put("userInfo", sysUserService.queryUserAuth(sysUser.getUsername())); result.put("perm", sysPermissionService.queryUserPermission(sysUser.getUsername())); return RestUtils.returnRes(true, this, result); }