原理
三大核心组件:Subject、SecurityManager、Realm
-
Subject
主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者; -
SecurityManager
安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器; -
Realm
域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。总结:
应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。
内部架构图:
整合
在springboot中整合shiro、redis和jwt,核心的配置:ShiroConfig、JwtFilter、ShiroRealm
,其中jwt主要是负责生成token的工具,redis负责缓存token。
首先我们配置Realm,然后配置filter及jwt工具类,再用shiroConfig来将这些配置联系起来,组成完整的认证鉴权系统。
准备工作
springboot版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/>
</parent>
jwt和shiro版本
<!--JWT-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
<exclusion>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
</exclusion>
</exclusions>
</dependency>
各项配置
-
ShiroRealm
主要负责认证(AuthenticationInfo)和鉴权(AuthorizationInfo)代码逻辑的实现。/** * 认证 * * @author zwj */ @Slf4j @Component public class ShiroRealm extends AuthorizingRealm { @Autowired private IUserService userService; @Autowired private StringRedisTemplate stringRedisTemplate; /** * 必须重写此方法,不然Shiro会报错 */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } /** * 授权(验证权限时调用) */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); return info; } /** * 认证(登录时调用) */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String accessToken = (String) token.getPrincipal(); if (accessToken == null) { throw new AuthenticationException(CommonCode.WEB_TOKEN_NULL.getMessage()); } // 校验token有效性 User tokenEntity = this.checkUserTokenIsEffect(accessToken); SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(tokenEntity, accessToken, getName()); return info; } /** * 校验token的有效性 * springboot2.3.+新增了一个配置项server.error.includeMessage,默认是NEVER, * 因此默认是不是输出message的,只要开启就可以了,否则无法拿到shiro抛出异常信息message * @param token */ public User checkUserTokenIsEffect(String token) throws AuthenticationException { // 解密获得username,用于和数据库进行对比 String userId = JwtUtil.getUserId(token); if (userId == null) { throw new AuthenticationException(CommonCode.WEB_TOKEN_ILLEGAL.getMessage()); } // 查询用户信息 User loginUser = userService.getById(userId); if (loginUser == null) { throw new AuthenticationException(CommonCode.WEB_USER_NOT_EXIST.getMessage()); } // 判断用户状态 if (loginUser.getStatus() != 0) { throw new LockedAccountException(CommonCode.WEB_ACCOUNT_LOCKED.getMessage()); } // 校验token是否超时失效 & 或者账号密码是否错误 if (!jwtTokenRefresh(token, userId, loginUser.getUserPhone())) { throw new IncorrectCredentialsException(CommonCode.WEB_TOKEN_FAILURE.getMessage()); } return loginUser; } /** * JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能) * 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 userId * @param userPhone * @return */ public boolean jwtTokenRefresh(String token, String userId, String userPhone) { //如果缓存中的token为空,直接返回失效异常 String cacheToken = stringRedisTemplate.opsForValue().get(CommonConstant.PREFIX_USER_TOKEN + token); if (!StrUtils.isBlank(cacheToken)) { // 校验token有效性 if (!JwtUtil.verify(cacheToken, userId, userPhone)) { JwtUtil.sign(userId, userPhone); } return true; } return false; } /** * 清除当前用户的权限认证缓存 * * @param principals 权限信息 */ @Override public void clearCache(PrincipalCollection principals) { super.clearCache(principals); } }
-
JwtFilter
这里会拦截需要认证和鉴权的请求,同时会捕获相应异常并抛出/** * 过滤器 * * @author zwj */ public class JwtFilter extends BasicHttpAuthenticationFilter { /** * 功能描述: 执行登录认证 * * @param request * @param response * @param mappedValue * @return boolean * @author zhouwenjie * @date 2021/12/24 14:45 */ @SneakyThrows @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { try { executeLogin(request, response); return true; } catch (Exception e) { throw new AuthenticationException(e.getMessage(), e); } } @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String token = JwtUtil.getTokenByRequest(httpServletRequest); 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; httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true"); 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")); // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } }
JwtToken
/** * token * * @author Mark sunlightcs@gmail.com */ public class JwtToken implements AuthenticationToken { private static final long serialVersionUID = 1L; private String token; public JwtToken(String token){ this.token = token; } @Override public String getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
JwtUtil:token工具类
/** * @Author zwj * @Desc JWT工具类 **/ public class JwtUtil { // Token过期时间180天(用户登录过期时间是此时间的两倍,以token在reids缓存时间为准) public static final long EXPIRE_TIME = 24 * 180 * 60 * 60 * 1000; public static final int days = 360; private static StringRedisTemplate stringRedisTemplate = SpringContextUtils.getBean(StringRedisTemplate.class); /** * 校验token是否正确 * * @param token 密钥 * @param userPhone 用户的密码 * @return 是否正确 */ public static boolean verify(String token, String userId, String userPhone) { try { // 根据密码生成JWT效验器 Algorithm algorithm = Algorithm.HMAC256(userPhone); JWTVerifier verifier = JWT.require(algorithm).withClaim("userId", userId).build(); // 效验TOKEN verifier.verify(token); return true; } catch (Exception exception) { return false; } } /** * 获得token中的信息无需secret解密也能获得 * * @return token中包含的用户名 */ public static String getUserId(String token) { try { DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("userId").asString(); } catch (Exception e) { return null; } } /** * 生成签名,360天后过期 * * @param userId 用户id * @param userPhone 用户的密码 * @return 加密的token */ public static String sign(String userId, String userPhone) { // Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(userPhone); // 附带userId信息 可以将user信息转成map存到这里 // String token = JWT.create().withClaim("userId", userId).withExpiresAt(date).sign(algorithm); String token = JWT.create().withClaim("userId", userId).sign(algorithm); stringRedisTemplate.opsForValue().set(CommonConstant.PREFIX_USER_TOKEN + token, token, days, TimeUnit.DAYS); return token; } /** * 根据request中的token获取用户账号 * * @param request * @return */ public static String getUserIdByToken(HttpServletRequest request) { String accessToken = getTokenByRequest(request); String userId = getUserId(accessToken); return userId; } /** * 获取 request 里传递的 token * * @param request * @return */ public static String getTokenByRequest(HttpServletRequest request) { String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); return token; } }
过期时间根据自己需求设定。文章来源:https://www.toymoban.com/news/detail-406916.html
-
ShiroConfig
整合各项配置的联系,注意新版本和老版本的配置区别,新版本需要重新注入beanDefaultAdvisorAutoProxyCreator、AuthorizationAttributeSourceAdvisor
,原因代码中也有详细注释。文章来源地址https://www.toymoban.com/news/detail-406916.html/** * Shiro配置 * * @author zwj */ @Configuration public class ShiroConfig { @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/web/login/**", "anon"); filterMap.put("/web/carOwner/list", "anon"); filterMap.put("/web/passengers/list", "anon"); filterMap.put("/web/user/sendSms", "anon"); filterMap.put("/web/sysDictionary/queryByIds", "anon"); filterMap.put("/web/user/addActive", "anon"); filterMap.put("/web/sysNotice/list", "anon"); filterMap.put("/web/sysAds/addViewNum", "anon"); filterMap.put("/web/sysAds/list", "anon"); filterMap.put("/web/sysAds/queryById", "anon"); filterMap.put("/web/sysArea/list", "anon"); //-------防止api文档被过滤掉 filterMap.put("/doc.html", "anon"); filterMap.put("/**/*.js", "anon"); filterMap.put("/**/*.css", "anon"); filterMap.put("/**/*.html", "anon"); filterMap.put("/**/*.svg", "anon"); filterMap.put("/**/*.pdf", "anon"); filterMap.put("/**/*.jpg", "anon"); filterMap.put("/**/*.png", "anon"); filterMap.put("/**/*.ico", "anon"); filterMap.put("/swagger-resources/**", "anon"); filterMap.put("/v2/api-docs", "anon"); filterMap.put("/v2/api-docs-ext", "anon"); filterMap.put("/webjars/**", "anon"); filterMap.put("/druid/**", "anon"); filterMap.put("/", "anon"); //=======防止api文档被过滤掉 filterMap.put("/**", "jwt"); //jwt过滤 Map<String, Filter> filters = new HashMap<>(); filters.put("jwt", new JwtFilter()); shiroFilter.setFilters(filters); shiroFilter.setFilterChainDefinitionMap(filterMap); return shiroFilter; } /** * 功能描述: 注入realm进行安全管理 * * @param shiroRealm * @return org.apache.shiro.web.mgt.DefaultWebSecurityManager * @author zhouwenjie * @date 2021/5/5 23:09 */ @Bean("securityManager") public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm,RedisProperties redisProperties) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroRealm); //关闭shiro自带的session存放token功能 DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO); //使用redis设置自定义缓存token securityManager.setCacheManager(redisCacheManager(redisProperties)); return securityManager; } /** * cacheManager 缓存 redis实现 * 使用的是shiro-redis开源插件 * * @return */ public RedisCacheManager redisCacheManager(RedisProperties redisProperties) { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager(redisProperties)); //redis中针对不同用户缓存(此处的id需要对应user实体中的userId字段,用于唯一标识) redisCacheManager.setPrincipalIdFieldName("id"); //用户权限信息缓存时间 redisCacheManager.setExpire(200000); return redisCacheManager; } /** * 配置shiro redisManager * 使用的是shiro-redis开源插件 * * @return */ @Bean public RedisManager redisManager(RedisProperties redisProperties) { RedisManager redisManager = new RedisManager(); redisManager.setHost(redisProperties.getHost()); redisManager.setPort(redisProperties.getPort()); redisManager.setTimeout(0); if (!StringUtils.isEmpty(redisProperties.getPassword())) { redisManager.setPassword(redisProperties.getPassword()); } return redisManager; /*IRedisManager manager; // redis 单机支持,在集群为空,或者集群无机器时候使用 add by jzyadmin@163.com if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) { RedisManager redisManager = new RedisManager(); redisManager.setHost(lettuceConnectionFactory.getHostName()); redisManager.setPort(lettuceConnectionFactory.getPort()); redisManager.setTimeout(0); if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) { redisManager.setPassword(lettuceConnectionFactory.getPassword()); } manager = redisManager; }else{ // redis 集群支持,优先使用集群配置 add by jzyadmin@163.com RedisClusterManager redisManager = new RedisClusterManager(); Set<HostAndPort> portSet = new HashSet<>(); lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort()))); JedisCluster jedisCluster = new JedisCluster(portSet); redisManager.setJedisCluster(jedisCluster); manager = redisManager; } return manager;*/ } @Bean("lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** *功能描述: 高版本shrio增加配置,否则类里方法上有@RequiresPermissions注解的,会导致整个类下的接口无法访问404 * @author zhouwenjie * @date 2021/12/29 9:08 * @param * @return org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator */ @Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
运用
/**
* 保存用户
*/
@ApiOperation(value = "保存用户", notes = "保存用户")
@SysLog("保存用户")
@PostMapping("/save")
@RequiresPermissions("sys:user:save")
public Result save(@RequestBody SysUserEntity user){
ValidatorUtils.validateEntity(user, ValidGroups.AddGroup.class);
user.setCreateUserId(getUserId());
sysUserService.saveUser(user);
return Result.ok();
}
到了这里,关于springboot整合shiro+jwt+redis详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!