SpringBoot整合 shiro + jwt,并会话共享

阳光穿透心脏的1/2处 2024-04-23 20:05 157阅读 0赞

SpringBoot整合 shiro + jwt,并会话共享

Shiro

Shrio的主要功能:

  • Authentication:用户认证(登录)
  • Authorization:权限控制
  • Session Management:会话管理
  • Cryptography:数据加密
  • Web Support:支持web的API
  • Caching:缓存
  • Concurrency:支持多线程应用程序
  • Testing:测试的支持
  • “Run As”:假设一个用户为另一个用户的身份
  • “Remember Me”:在Session中保存用户身份

基本原理

Shiro的基本架构:

在这里插入图片描述

Shiro有三个核心的概念:Subject、SecurityManager 和 Realms。

  • Subject:Subject实质上是一个当前执行用户的特定的安全“视图”,开发者所写的应用代码就通过Subject与Shiro框架进行交互。所有Subject实例都必须绑定到一个SecurityManager上,当使用一个Subject实例时,Subject实例会和SecurityManager进行交互,完成相应操作。
  • SecurityManager:SecurityManager是Shiro的核心部分,作为一种“保护伞”对象来协调内部安全组件共同构成一个对象图。开发人员并不直接操作SecurityManager,而是通过Subject来操作SecurityManager来完成各种安全相关操作。
  • Realms:Realms担当Shiro和应用程序的安全数据之间的“桥梁”或“连接器”。从本质来讲,Realm是一个特定安全的DAO,Realm中封装了数据操作的模块和用户自定义的认证匹配过程。SecurityManager可能配置多个Realms,但至少要有一个。

使用SpringBoot整合

1. 导入 shiro-redis 的starter 的依赖,还有 jwt 工具包,以及为了简化开发,引入 hutool工具包
  1. <!-- 整合shiro + jwt -->
  2. <dependency>
  3. <groupId>org.crazycake</groupId>
  4. <artifactId>shiro-redis-spring-boot-starter</artifactId>
  5. <version>3.3.1</version>
  6. </dependency>
  7. <!-- 为了简化开发,这里引入了hutool工具类 -->
  8. <dependency>
  9. <groupId>cn.hutool</groupId>
  10. <artifactId>hutool-all</artifactId>
  11. <version>5.7.22</version>
  12. </dependency>
  13. <!-- jwt -->
  14. <dependency>
  15. <groupId>io.jsonwebtoken</groupId>
  16. <artifactId>jjwt</artifactId>
  17. <version>0.9.1</version>
  18. </dependency>
2. 编写配置

ShiroConfig

  • com.xxx.config.ShiroConfig

    /**

    • shiro启用注解拦截控制器
      */
      @Configuration
      public class ShiroConfig {
  1. @Autowired
  2. private JwtFilter jwtFilter;
  3. @Bean
  4. public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
  5. DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
  6. // 注入 redisSessionDAO
  7. sessionManager.setSessionDAO(redisSessionDAO);
  8. return sessionManager;
  9. }
  10. /**
  11. * 创建安全管理器
  12. * AccountRealm--是 shiro 进行【登录】或者【权限校验】的逻辑所在,算是核心了,需要重写3个方法,分别是:
  13. * supports(): 为了让 realm 支持 jwt 的凭证校验
  14. * doGetAuthorizationInfo(): 权限校验
  15. * doGetAuthenticationInfo(): 登录认证校验
  16. * @param sessionManager
  17. * @param redisCacheManager
  18. *
  19. */
  20. @Bean
  21. public DefaultWebSecurityManager securityManager(AccountRealm accountRealm,
  22. SessionManager sessionManager,
  23. RedisCacheManager redisCacheManager) {
  24. // 创建安全管理器对象
  25. DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
  26. // 注入 sessionManager
  27. securityManager.setSessionManager(sessionManager);
  28. // 关闭 shiro 自带的 session,这样用户就不能再通过 session 方式登录 shiro,后面将采用 jwt 凭证登录。
  29. DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
  30. DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
  31. defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
  32. subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
  33. securityManager.setSubjectDAO(subjectDAO);
  34. // 注入 redisCacheManager
  35. securityManager.setCacheManager(redisCacheManager);
  36. return securityManager;
  37. }
  38. /**
  39. * 在 ShiroFilterChainDefinition 中,我们不在通过编码形式拦截 Controller 的访问路径,而是所有的
  40. * 路由都需要经过 JwtFilter 这个过滤器,然后判断请求头中是否含有 jwt 的信息,有就登录,没有就跳过。
  41. * 跳过之后,由 Controller 中的 shiro注解 进行再次拦截,比如 @RequiresAuthentication,从而控制权限访问。
  42. *
  43. */
  44. @Bean
  45. public ShiroFilterChainDefinition shiroFilterChainDefinition() {
  46. DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
  47. Map<String, String> filterMap = new LinkedHashMap<>();
  48. filterMap.put("/**", "jwt");
  49. chainDefinition.addPathDefinitions(filterMap);
  50. return chainDefinition;
  51. }
  52. /**
  53. * 创建 shiroFilter 负责拦截所有请求
  54. * @param securityManager
  55. * @param chainDefinition
  56. * @return
  57. */
  58. @Bean("shiroFilterFactoryBean")
  59. public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
  60. ShiroFilterChainDefinition chainDefinition) {
  61. ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
  62. // 给 shiroFilter 设置安全管理器
  63. shiroFilter.setSecurityManager(securityManager);
  64. //配置系统受限资源
  65. //配置系统公共资源
  66. Map<String, Filter> filters = new HashMap<>();
  67. // 使用 jwtFilter 过滤器
  68. filters.put("jwt", jwtFilter);
  69. shiroFilter.setFilters(filters);
  70. Map<String, String> filterMap = shiroFilterChainDefinition().getFilterChainMap();
  71. shiroFilter.setFilterChainDefinitionMap(filterMap);
  72. return shiroFilter;
  73. }
  74. /**
  75. * 解决 aop 与 shiro 冲突问题
  76. */
  77. @Bean
  78. public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
  79. DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
  80. defaultAdvisorAutoProxyCreator.setUsePrefix(true);
  81. return defaultAdvisorAutoProxyCreator;
  82. }
  83. }

上面的 ShiroConfig,主要做了几件事情:

  • 引入 RedisSessionDao 和 RedisCacheManager,为了解决 shiro 的权限数据和会话信息能保存到 redis 中,实现会话共享。
  • 重写了 SessionManager 和 DefaultWebSecurityManager,同时在 DefaultWebSecurityManager 中关闭了 shiro 自带的 session 方式,将其设置为 false,这样用户就不能再通过 session 方式登录 shiro。后面会采用 jwt 凭证登录。
  • 在 ShiroFilterChainDefinition 中,不再通过编码形式拦截请求访问路径,而是所有路由都需要经过 JwtFilter 这个过滤器,然后判断请求头中是否含有 jwt 的信息,有就登录,没有就跳过。跳过之后,由 Controller 中的 shiro 注解再次进行拦截,比如 @RequiresAuthentication,来控制权限访问。

接下来就是 ShiroConfig 出现的 AccountRealm,还有 JwtFilter。

AccountRealm:

AccountRealm 是 shiro 进行登录或者权限校验的逻辑所在,需要重写3个方法:

  • supports:为了让 realm 支持 jwtToken 凭证校验,因为shiro 默认支持的是UsernamePasswordToken,所以要自定义一个新类 JwtToken。

com.xxx.shiro.JwtToken

  1. /**
  2. * 自定义 JwtToken类,来完成 shiro 的 supports 方法
  3. */
  4. public class JwtToken implements AuthenticationToken {
  5. private String token;
  6. public JwtToken(String token) {
  7. this.token = token;
  8. }
  9. @Override
  10. public Object getPrincipal() {
  11. return token;
  12. }
  13. @Override
  14. public Object getCredentials() {
  15. return token;
  16. }
  17. }
  • doGetAuthenticationInfo:认证
  • doGetAuthorizationInfo:授权

com.xxx.shiro.AccountRealm

  1. /**
  2. * 登录认证和授权
  3. * 自定义Realm
  4. */
  5. @Slf4j
  6. @Component
  7. public class AccountRealm extends AuthorizingRealm {
  8. @Autowired
  9. private JwtUtils jwtUtils;
  10. @Autowired
  11. private UserService userService;
  12. /**
  13. * shiro 默认 supports 的是UsernamePasswordToken,而我们现在采用的是 jwt 方式,
  14. * 所以这里 自定义一个新类 JwtToken,来完成 shiro 的 supports 方法。
  15. */
  16. @Override
  17. public boolean supports(AuthenticationToken token) {
  18. return token instanceof JwtToken;
  19. }
  20. /**
  21. * 认证
  22. * @param token 包含用户名和密码的令牌
  23. */
  24. @Override
  25. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  26. JwtToken jwt = (JwtToken) token;
  27. log.info("jwt------------------>{ }", jwt);
  28. // 解析JWTtoken,从令牌 token 中拿到 用户id 和 用户名
  29. String userId = (String) jwtUtils.parseJWT((String) jwt.getPrincipal()).get("userId");
  30. String username = (String) jwtUtils.parseJWT((String) jwt.getPrincipal()).get("username");
  31. // 根据 用户id 查询用户
  32. User user = userService.getById(userId);
  33. if (user == null) {
  34. throw new UnknownAccountException("账户不存在!");
  35. }
  36. if (user.getStatus() == -1) {
  37. throw new LockedAccountException("账户已被锁定!");
  38. }
  39. if (!user.getUsername().equals(username)) {
  40. throw new UnknownAccountException("用户名错误!");
  41. }
  42. // 比较密码
  43. // 登录成功后返回的用户信息的实体
  44. AccountProfile profile = new AccountProfile();
  45. BeanUtil.copyProperties(user, profile);
  46. log.info("profile----------------->{}", profile.toString());
  47. // 返回认证信息 参数1:用户身份信息 参数2:加密后的密码 参数3:realm的名字
  48. return new SimpleAuthenticationInfo(profile, jwt.getCredentials(), getName());
  49. }
  50. /**
  51. * 授权
  52. * @param principals 身份集合信息
  53. * @return
  54. */
  55. @Override
  56. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  57. log.info("执行doGetAuthorizationInfo方法进行授权");
  58. // String username = JwtUtil.getUsername(principalCollection.toString());
  59. log.info(principals.toString());
  60. // log.info("登录的用户:" + username);
  61. SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
  62. // 获取当前登录用户的【主身份信息】
  63. AccountProfile accountProfile = (AccountProfile) principals.getPrimaryPrincipal();
  64. // 拿到当前用户的所有角色(因为一个用户可以有多个角色)
  65. String[] roles = accountProfile.getRole().split(",");
  66. log.info("roles");
  67. // 根据用户的角色,来对用户进行授权
  68. for (String role : roles) {
  69. info.addRole(role);
  70. if (role.equals("role_root")) {
  71. info.addStringPermission("user:create");
  72. info.addStringPermission("user:update");
  73. info.addStringPermission("user:read");
  74. info.addStringPermission("user:delete");
  75. } else if (role.equals("role_admin")) {
  76. info.addStringPermission("user:read");
  77. info.addStringPermission("user:create");
  78. info.addStringPermission("user:update");
  79. } else if (role.equals("role_user")) {
  80. info.addStringPermission("user:read");
  81. info.addStringPermission("user:create");
  82. } else if (role.equals("role_guest")) {
  83. info.addStringPermission("user::read");
  84. }
  85. }
  86. // 返回权限信息
  87. return info;
  88. }
  89. }
  • JwtUtils 是个生成和解析 JwtToken 的工具类,其中有些 jwt 相关的密钥信息是从配置文件中配置的。

com.xxx.util.JwtUtils

  1. /**
  2. * jwt (Json Web Token)工具类
  3. * 用于 创建jwt字符串 和 解析jwt。
  4. */
  5. @Slf4j
  6. @Data
  7. @Component
  8. @ConfigurationProperties(prefix = "xxx.jwt")
  9. public class JwtUtils {
  10. private String secret;
  11. private long expire;
  12. private String header;
  13. /**
  14. * 生成 Jwt Token 字符串
  15. * @param userId 签发人id
  16. * expireDate 过期时间 签发时间
  17. * claims 额外添加到荷部分的信息。
  18. * 例如可以添加用户名、用户ID、用户(加密前的)密码等信息
  19. */
  20. public String createJWT(long userId, String username) {
  21. Date nowDate = new Date();
  22. // 过期时间
  23. Date expireDate = new Date(nowDate.getTime() + expire * 1000);
  24. //创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
  25. Map<String, Object> claims = new HashMap<String, Object>();
  26. claims.put("userId", userId+"");
  27. claims.put("username",username);
  28. return Jwts.builder() // 创建 JWT 对象
  29. .setHeaderParam("typ", "JWT") // 设置头部信息
  30. .setClaims(claims) // 设置私有声明
  31. .setIssuedAt(nowDate) // 设置payload的签发时间
  32. .setExpiration(expireDate) // 这是payload的过期时间
  33. .signWith(SignatureAlgorithm.HS512, secret)// 设置安全密钥(生成签名所需的密钥和算法)
  34. .compact(); // 生成JWT token (1.编码 Header 和 Payload 2.生成签名 3.拼接字符串)
  35. }
  36. /**
  37. * 解析 token
  38. * JWT Token 由 头部 荷载部 和 签名部 三部分组成。签名部分是由加密算法生成,无法反向解密。
  39. * 而 头部 和 荷载部分是由 Base64 URL算法生成,是可以反向反编码回原样的。
  40. * 这也是为什么不要在 JWT Token 中放敏感数据的原因。
  41. *
  42. * @param token 加密后的token
  43. * @return claims 返回荷载部分的键值对
  44. */
  45. public Claims parseJWT(String token) {
  46. try {
  47. return Jwts.parser() // 创建解析对象
  48. .setSigningKey(secret) // 设置安全密钥(生成签名所需的密钥和算法)
  49. .parseClaimsJws(token) // 解析 token
  50. .getBody(); // 获取 payload 部分内容
  51. } catch (Exception e) {
  52. log.debug("validate is token error ", e);
  53. return null;
  54. }
  55. }
  56. /**
  57. * token 是否过期
  58. * @return return true: 过期
  59. */
  60. public boolean isTokenExpired(Date expiration) {
  61. return expiration.before(new Date());
  62. }
  63. }
  • 在 AccountRealm 中还用到了 AccountProfile,这是为了登录成功之后返回的一个用户信息的载体。

com.xxx.shiro.AccountProfile

  1. **
  2. * 用于登陆成功后返回的一个【用户信息的载体/实体】
  3. * avatar 用户头像
  4. */
  5. @Data
  6. public class AccountProfile implements Serializable {
  7. /**
  8. * 用户id
  9. */
  10. private Long id;
  11. /**
  12. * 用户名
  13. */
  14. private String username;
  15. /**
  16. * 用户头像
  17. */
  18. private String avatar;
  19. /**
  20. * 用户角色
  21. */
  22. private String role;
  23. }
3. 基本的校验完成后,进行少量的信息配置

application.yml:

  1. shiro-redis:
  2. enabled: true
  3. redis-manager:
  4. host: 127.0.0.0
  5. xxx:
  6. jwt:
  7. # 加密密钥, 部署上线务必修改此配置,以保证token的安全性
  8. secret: xxxxxxx
  9. expire: 172800
  10. header: token
4. 定义 jwt 的过滤器 JwtFilter

这个过滤器是重点,这里继承的是Shiro内置的AuthenticatingFilter,一个可以内置自动登录方法的过滤器,也可以继承BasicHttpAuthenticationFilter。

需要重写几个方法:

  1. 1) createToken:实现登录,需要生成我们自定义支持的 JwtToken
  2. 2) onAccessDenied:拦截校验,当头部没有Authorization,直接通过,不需要自动登录;当头部带有的时候,首先要验证 jwt 的有效性,没问题就直接执行 executeLogin 方法自动登录。
  3. 3) onLoginFailure:登陆异常的时候进入的方法,我们直接把异常信息封装然后抛出
  4. 4) preHandle:拦截器的前置拦截,因为是前后端分离项目,项目中除了选用跨域全局配置之外,我们在拦截其中也要提供跨域支持。这样拦截器就不会在进入Controller之前就被限制了。

com.xxx.shiro.JwtFilter总体代码:

  1. @Component
  2. public class JwtFilter extends AuthenticatingFilter {
  3. @Autowired
  4. private JwtUtils jwtUtils;
  5. @Override
  6. protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
  7. // 获取 Token
  8. HttpServletRequest request = (HttpServletRequest) servletRequest;
  9. String jwtToken = request.getHeader("Authorization");
  10. if (StringUtils.isEmpty(jwtToken)) {
  11. return null;
  12. }
  13. return new JwtToken(jwtToken);
  14. }
  15. @Override
  16. protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
  17. HttpServletRequest request = (HttpServletRequest) servletRequest;
  18. String token = request.getHeader("Authorization");
  19. // 当头部没有 Authorization的时候,直接通过,不需要自动登录;
  20. if (StringUtils.isEmpty(token)) {
  21. return true;
  22. } else {
  23. // 当头部带有的时候,首先要验证 jwt 的有效性,没问题就直接执行 executeLogin 方法自动登录
  24. // 判断是否已经过期
  25. Claims claims = jwtUtils.parseJWT(token);
  26. if (claims == null || jwtUtils.isTokenExpired(claims.getExpiration())) {
  27. throw new ExpiredCredentialsException("token已失效,请重新登录!");
  28. }
  29. }
  30. // 没有失效就执行自动登录
  31. return executeLogin(servletRequest, servletResponse);
  32. }
  33. /**
  34. * 登陆异常的时候进入的方法,我们`在这里插入代码片`直接把异常信息封装然后抛出
  35. */
  36. @Override
  37. protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
  38. HttpServletResponse httpResponse = (HttpServletResponse) response;
  39. // 处理登录失败的异常
  40. try {
  41. Throwable throwable = e.getCause() == null ? e : e.getCause();
  42. Result re = Result.fail(throwable.getMessage());
  43. String json = JSONUtil.toJsonStr(re);
  44. httpResponse.getWriter().print(json);
  45. } catch (IOException ex) {
  46. }
  47. return false;
  48. }
  49. /**
  50. * 对跨域提供支持
  51. */
  52. @Override
  53. protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
  54. HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
  55. HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
  56. httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
  57. httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
  58. httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
  59. // 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
  60. if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
  61. httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
  62. return false;
  63. }
  64. return super.preHandle(request, response);
  65. }
  66. }

到这里 shiro 就已经完成了整合,并且使用了 jwt 进行身份验证。

发表评论

表情:
评论列表 (有 0 条评论,157人围观)

还没有评论,来说两句吧...

相关阅读