当前位置:   article > 正文

SpringBoot + Shiro + Jwt 实现登录认证,代码分析_springboot+jwt+shiro+uniapp实现跨域登录

springboot+jwt+shiro+uniapp实现跨域登录

目录

前言

思路分析

核心代码分析

一、依赖引入和数据库表

二、注册

三、密码登录

四、携带jwt访问

五、Shiro的自定义配置类


前言

花了几天了解Shiro框架(也不算太深入),根据网上资料做了一个Demo:SpringBoot 2.2.9.RELEASE+ Shiro + Jwt 实现登录认证。

1、这个Demo关注 登录授权(比较通用),基本不涉及权限控制,因为权限控制设计到具体业务。所以可以本Demo可以根据自身实际情况稍加修改,就可以作为前后端分离项目的登录模块。

2、本博客适用于对Shiro和Jwt有了解,起码对于几个关键的类的作用及方法有了解,下面我不会展开,只会对Demo的思路和代码做分析。

思路分析

1、Shiro原本的登录认证的流程

subject.login(new UsernamePasswordToken(username, password)); 这是执行登录认证的关键代码,然后Shiro内部会在对应的Realm里面的doGetAuthenticationInfo()中根据username从数据库中查找正确的User信息(包括密码信息,密码可以被加密存储)。然后将正确的User信息封装成AuthenticationInfo对象。在比较器(CredentialsMatcher)的doCredentialsMatch()方法中按照一定规则进行密码匹配(比较)。如果匹配成功,就登录成功。

登录成功后,subject的登录信息会被存储session中。那么下次访问受限的资源或接口,便不需要再登陆了。

2、加入Jwt后的登录认证的流程

Jwt和Shiro原本的登录认证有冲突吗? 其实Jwt本质上就是一种特殊的token而已。换言之,Shiro + Jwt的意思就是使用token(Jwt)替换到Shiro原本的session,使用token的服务端更加适用于前后端分离的项目。不管前端是vue项目还是Android APP等等。

替换之后的流程是什么样的? 首先,第一次登录,我们需要用户输入username和password(根据实际情况,此处简单举例),然后按照Shiro原本的登录认证流程登录。如果登录成功,服务端返回一个Jwt字符串(相当于签发一个令牌)给前端。下次用户访问服务端受限的资源,只要携带正确合法的Jwt就可以访问了。

实现的主要思路是怎样的?我们需要实现一个过滤器,拦截所有请求,对于请求头中含有Jwt的请求进行特殊处理,我们不使用Shiro默认登录流程来处理,而是使用我们自定义的处理流程,包括实现对应的Token,Realm,CredentialsMatcher。当认证通过之后,将请求放行到Controller层。

 经过一顿分析,本Demo主要划分3个部分:注册密码登录携带令牌(jwt)访问。其中,“携带令牌(jwt)访问”这个部分就是Shiro整合Jwt的核心逻辑。

核心代码分析

一、依赖引入和数据库表

数据库相关等其他依赖我就不贴出来了,详情参照Demo的Github源码:https://github.com/passerbyYSQ/SpringBoot_Shiro_Jwt

推荐】Shiro的项目实战使用参考(这个项目打磨了比较久,Shiro的使用可参考该项目):

GitHub - passerbyYSQ/forum: Forum(社区&论坛),分布式项目。由于毕设需要,正在逐渐集成即时通讯功能(视频弹幕和在线聊天)。希望各位大佬可以一起贡献代码,一起完善这个凝聚了我很多心血和时间的开源作品!

  1. <!-- apache为SpringBoot整合shiro提供的starter -->
  2. <dependency>
  3. <groupId>org.apache.shiro</groupId>
  4. <artifactId>shiro-spring-boot-starter</artifactId>
  5. <version>1.5.3</version>
  6. </dependency>
  7. <!-- Shiro默认的缓存管理 -->
  8. <dependency>
  9. <groupId>org.apache.shiro</groupId>
  10. <artifactId>shiro-ehcache</artifactId>
  11. <version>1.5.3</version>
  12. </dependency>
  13. <!-- JWT -->
  14. <dependency>
  15. <groupId>com.auth0</groupId>
  16. <artifactId>java-jwt</artifactId>
  17. <version>3.3.0</version>
  18. </dependency>

二、注册

注册的时候,我们需要将用户输入的明文密码通过Md5Hash加密处理,传入加密的随机盐(每个用户不一样,需要存到用户表)和哈希散列的次数(每个用户都一样)。

那么在使用密码登录时,我们需要以相同的加密规则加密用户输入的密码,然后与数据库中存储的注册时的加密密码,进行比对。如果一样,登录成功。

UserController

  1. /**
  2. * 通过注解实现权限控制
  3. * 在方法前添加注解 @RequiredRoles@RequiredPermissions
  4. *
  5. * @author passerbyYSQ
  6. * @create 2020-08-20 23:54
  7. */
  8. @Controller
  9. @RequestMapping("/user")
  10. public class UserController {
  11. @Autowired
  12. private UserService userService;
  13. /**
  14. * 用户注册
  15. * @param user
  16. * @return
  17. */
  18. @PostMapping("/register")
  19. public ResponseEntity<String> register(User user) {
  20. // 参数判断省略
  21. // ...
  22. try {
  23. userService.register(user);
  24. return ResponseEntity.ok().build();
  25. } catch (Exception e) {
  26. e.printStackTrace();
  27. }
  28. // 错误提示信息省略...
  29. return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body("客户端传参错误");
  30. }
  31. }

UserServiceImpl

  1. /**
  2. * @author passerbyYSQ
  3. * @create 2020-08-21 11:02
  4. */
  5. @Service("userService")
  6. @Transactional // 开启事务。有需要再开启
  7. public class UserServiceImpl implements UserService {
  8. @Autowired
  9. private UserDAO userDAO;
  10. @Override
  11. public void register(User user) {
  12. // 8个字符的随机字符串,作为加密的随机盐
  13. String salt = RandomUtil.generateStr(8);
  14. // 需要保存到数据库,第一次登录(认证)比较时需要使用
  15. user.setSalt(salt);
  16. // Md5Hash默认将随机盐拼接到源字符串的前面,然后使用md5加密,再经过x次的哈希散列
  17. // 第三个参数(hashIterations):哈希散列的次数
  18. Md5Hash md5Hash = new Md5Hash(user.getPassword(), user.getSalt(), 1024);
  19. user.setPassword(md5Hash.toHex());
  20. // 保存
  21. userDAO.save(user);
  22. }
  23. }

三、密码登录

UserController

  1. /**
  2. * 通过注解实现权限控制
  3. * 在方法前添加注解 @RequiredRoles@RequiredPermissions
  4. *
  5. *
  6. * @author passerbyYSQ
  7. * @create 2020-08-20 23:54
  8. */
  9. @Controller
  10. @RequestMapping("/user")
  11. public class UserController {
  12. @Autowired
  13. private UserService userService;
  14. /**
  15. * 用户登录(身份认证)
  16. * Shiro会缓存认证信息
  17. *
  18. * @param username
  19. * @param password
  20. * @return
  21. */
  22. @PostMapping("/login")
  23. public ResponseEntity<String> login(String username, String password) {
  24. // 前期的注入工作已经由SpringBoot完成了
  25. // 获取当前来访用户的主体对象
  26. Subject subject = SecurityUtils.getSubject();
  27. try {
  28. // 执行登录,如果登录失败会直接抛出异常,并进入对应的catch
  29. subject.login(new UsernamePasswordToken(username, password));
  30. // 获取主体的身份信息
  31. // 实际上是User。为什么?
  32. // 取决于LoginRealm中的doGetAuthenticationInfo()方法中SimpleAuthenticationInfo构造函数的第一个参数
  33. User user = (User) subject.getPrincipal();
  34. // 生成jwt
  35. String jwt = userService.generateJwt(user.getUsername());
  36. // 将jwt放入到响应头中
  37. return ResponseEntity.ok().header("token", jwt).build();
  38. } catch (UnknownAccountException e) {
  39. // username 错误
  40. e.printStackTrace();
  41. return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("username不存在");
  42. } catch (IncorrectCredentialsException e) {
  43. // password 错误
  44. e.printStackTrace();
  45. return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("password错误");
  46. }
  47. }
  48. /**
  49. * 退出登录
  50. * 销毁主体的认证记录(信息),下次访问需要重新认证
  51. *
  52. * @return
  53. */
  54. @RequestMapping("/logout")
  55. public ResponseEntity<String> logout() {
  56. Subject subject = SecurityUtils.getSubject();
  57. User user = (User) subject.getPrincipal();
  58. userService.logout(user.getUsername());
  59. subject.logout();
  60. return ResponseEntity.ok().build();
  61. }
  62. }

UserServiceImpl

  1. /**
  2. * @author passerbyYSQ
  3. * @create 2020-08-21 11:02
  4. */
  5. @Service("userService") // 不要忘了
  6. @Transactional // 开启事务。有需要再开启
  7. public class UserServiceImpl implements UserService {
  8. @Autowired
  9. private UserDAO userDAO;
  10. @Override
  11. public String generateJwt(String username) {
  12. // 8个字符的随机字符串,作为生成jwt的随机盐
  13. // 保证每次登录成功返回的Token都不一样
  14. String jwtSecret = RandomUtil.generateStr(8);
  15. // 将此次登录成功的jwt secret存到数据库,下次携带jwt时解密需要使用
  16. userDAO.updateJwtSecretByUsername(username, jwtSecret);
  17. return JwtUtil.generateJwt(username, jwtSecret);
  18. }
  19. @Override
  20. public User findByUsername(String username) {
  21. return userDAO.findByUsername(username);
  22. }
  23. @Override
  24. public void logout(String username) {
  25. // 将jwt secret置为空
  26. userDAO.updateJwtSecretByUsername(username, "");
  27. }
  28. }

LoginRealm:处理密码登录的Realm,复写doGetAuthenticationInfo()方法

  1. /**
  2. * @author passerbyYSQ
  3. * @create 2020-08-20 23:31
  4. */
  5. public class LoginRealm extends AuthorizingRealm {
  6. /*
  7. 如果@Autowired,需要在当前类前加上@Componet注解,将当前类的实例注入到IOC容器
  8. 但是如果有多个类似的类,都要注册到容器中,不太好。我可以新建一个管理类,注册到容器中,
  9. 为我们统一获取@Autowired的实例
  10. */
  11. // @Autowired
  12. // private UserService userService;
  13. /**
  14. * 或者在ShiroConfig中设置
  15. */
  16. public LoginRealm() {
  17. // 匹配器。需要与密码加密规则一致
  18. HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
  19. // 设置匹配器的加密算法
  20. hashedCredentialsMatcher.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
  21. // 设置匹配器的哈希散列次数
  22. hashedCredentialsMatcher.setHashIterations(1024);
  23. // 将对应的匹配器设置到Realm中
  24. this.setCredentialsMatcher(hashedCredentialsMatcher);
  25. }
  26. /**
  27. * 可以往Shiro中注册多种Realm。某种Token对象需要对应的Realm来处理。
  28. * 复写该方法表示该方法支持处理哪一种Token
  29. * @param token
  30. * @return
  31. */
  32. @Override
  33. public boolean supports(AuthenticationToken token) {
  34. return token instanceof UsernamePasswordToken;
  35. }
  36. @Override
  37. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  38. return null;
  39. }
  40. @Override
  41. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  42. // 从Token中获取身份信息。这里实际上是username,这里从UsernamePasswordToken的源码可以看出
  43. String principal = (String) token.getPrincipal();
  44. // 从IOC容器中获取UserService组件
  45. UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
  46. User user = userService.findByUsername(principal);
  47. if (!ObjectUtils.isEmpty(user)) {
  48. // 返回正确的信息(数据库存储的),作为比较的基准
  49. return new SimpleAuthenticationInfo(
  50. user, user.getPassword(),
  51. ByteSource.Util.bytes(user.getSalt()), this.getName()
  52. );
  53. }
  54. return null;
  55. }
  56. }

密码登录的主要流程基本就这么多。但是还需要写配置类将LoginRealm注册到Shiro中,这个后面统一把 Shiro 的配置类贴出来。上面代码还涉及到两个工具类:

ApplicationContextUtil:用于更加灵活地获取IOC容器中组件

JwtUtil:jwt的工具类,提供几个主要的静态方法,包括:生成jwt,校验jwt,获取jwt里面的数据,获取jwt的签发时间。

ApplicationContextUtil

  1. /**
  2. * @author passerbyYSQ
  3. * @create 2020-08-21 11:50
  4. */
  5. @Component // 加入容器
  6. public class ApplicationContextUtil implements ApplicationContextAware {
  7. // IOC容器
  8. private static ApplicationContext context;
  9. /**
  10. * 将IOC容器回调给我们,我们将它缓存起来
  11. *
  12. * @param applicationContext
  13. * @throws BeansException
  14. */
  15. @Override
  16. public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
  17. context = applicationContext;
  18. }
  19. /**
  20. * 从IOC容器中获取组件(bean)
  21. *
  22. * @param beanName
  23. * @return
  24. */
  25. public static Object getBean(String beanName) {
  26. return context.getBean(beanName);
  27. }
  28. }

JwtUtil

  1. /**
  2. * JWT的工具类,包括签发、验证、获取信息
  3. *
  4. * @author passerbyYSQ
  5. * @create 2020-08-22 11:13
  6. */
  7. public class JwtUtil {
  8. // 有效时间:7天
  9. private static final long EFFECTIVE_DURATION = 1000 * 60 * 60 * 24 * 7;
  10. // 发行者
  11. private static final String ISSUER = "net.ysq";
  12. /**
  13. * 生成Jwt字符串
  14. *
  15. * @param claims 由于类库只支持基本类型的包装类、String、Date,我们最好使用String
  16. * @param secret 加密的密钥
  17. * @return
  18. */
  19. public static String generateJwt(Map<String, String> claims, String secret) {
  20. // 发行时间
  21. Date issueAt = new Date();
  22. // 过期时间
  23. Date expireAt = new Date(issueAt.getTime() + EFFECTIVE_DURATION);
  24. // 加密算法
  25. Algorithm algorithm = Algorithm.HMAC256(secret.getBytes(StandardCharsets.UTF_8));
  26. JWTCreator.Builder builder = JWT.create()
  27. .withIssuer(ISSUER)
  28. .withIssuedAt(issueAt)
  29. .withExpiresAt(expireAt);
  30. // 设置Payload信息
  31. Set<String> keySet = claims.keySet();
  32. for (String key : keySet) {
  33. builder.withClaim(key, claims.get(key));
  34. }
  35. return builder.sign(algorithm);
  36. }
  37. public static String generateJwt(String username, String secret) {
  38. Map<String, String> claims = new HashMap<>();
  39. claims.put("username", username);
  40. return generateJwt(claims, secret);
  41. }
  42. /**
  43. * 校验jwt是否合法
  44. *
  45. * @param jwt
  46. * @param claims
  47. * @return
  48. */
  49. public static boolean verifyJwt(String jwt, Map<String, String> claims, String secret) {
  50. // 解密算法
  51. Algorithm algorithm = Algorithm.HMAC256(secret.getBytes(StandardCharsets.UTF_8));
  52. try {
  53. Verification verification = JWT.require(algorithm).withIssuer(ISSUER);
  54. Set<String> keySet = claims.keySet();
  55. for (String key : keySet) {
  56. verification.withClaim(key, claims.get(key));
  57. }
  58. JWTVerifier verifier = verification.build();
  59. verifier.verify(jwt);
  60. return true;
  61. } catch (IllegalArgumentException | JWTVerificationException e) {
  62. e.printStackTrace();
  63. }
  64. return false;
  65. }
  66. public static boolean verifyJwt(String jwt, String username, String secret) {
  67. Map<String, String> claims = new HashMap<>();
  68. claims.put("username", username);
  69. return verifyJwt(jwt, claims, secret);
  70. }
  71. /**
  72. * 根据key获取claim值
  73. *
  74. * @param jwt
  75. * @param key
  76. * @return
  77. */
  78. public static String getClaimByKey(String jwt, String key) {
  79. try {
  80. DecodedJWT decodedJwt = JWT.decode(jwt);
  81. return decodedJwt.getClaim(key).asString(); // 注意不要用toString
  82. } catch (JWTDecodeException e) {
  83. e.printStackTrace();
  84. }
  85. return null;
  86. }
  87. /**
  88. * 返回过期的时间
  89. *
  90. * @param jwt
  91. * @return
  92. */
  93. public static Date getExpireAt(String jwt) {
  94. try {
  95. DecodedJWT decodedJwt = JWT.decode(jwt);
  96. return decodedJwt.getExpiresAt();
  97. } catch (JWTDecodeException e) {
  98. e.printStackTrace();
  99. }
  100. return null;
  101. }
  102. }

四、携带jwt访问

JwtToken:类比UsernamePasswordToken

  1. /**
  2. * 或者直接实现AuthenticationToken也可以,不需要host
  3. *
  4. * @author passerbyYSQ
  5. * @create 2020-08-22 10:42
  6. */
  7. public class JwtToken implements HostAuthenticationToken {
  8. // JWT字符串
  9. private String token;
  10. private String host;
  11. public JwtToken(String token) {
  12. this(token, null);
  13. }
  14. public JwtToken(String token, String host) {
  15. this.token = token;
  16. this.host = host;
  17. }
  18. @Override
  19. public String getHost() {
  20. return token;
  21. }
  22. /**
  23. * 返回身份信息(相当于username),这个方法的返回比较重要,前面的代码也说到了
  24. * Jwt里面包含一个访问主体的身份(比如说username)
  25. * @return
  26. */
  27. @Override
  28. public Object getPrincipal() {
  29. return token;
  30. }
  31. /**
  32. * 返回凭证信息(相当于password)
  33. * Jwt本身就是一个令牌凭证,在服务端通过解密校验
  34. * @return
  35. */
  36. @Override
  37. public Object getCredentials() {
  38. return token;
  39. }
  40. public String getToken() {
  41. return token;
  42. }
  43. public void setToken(String token) {
  44. this.token = token;
  45. }
  46. public void setHost(String host) {
  47. this.host = host;
  48. }
  49. }

JwtAuthenticatingFilter:全局请求的过滤器。驳回没有携带token的请求,不能访问受限资源。对于携带token的请求,校验token是否有效,有效最重放行到controller层

  1. /**
  2. * @author passerbyYSQ
  3. * @create 2020-08-22 12:06
  4. */
  5. public class JwtAuthenticatingFilter extends BasicHttpAuthenticationFilter {
  6. // 是否刷新token
  7. private boolean shouldRefreshToken;
  8. public JwtAuthenticatingFilter() {
  9. this.shouldRefreshToken = false;
  10. }
  11. /**
  12. * 请求是否允许放行
  13. * 父类会在请求进入拦截器后调用该方法,返回true则继续,返回false则会调用onAccessDenied()。这里在不通过时,还调用了isPermissive()方法,我们后面解释。
  14. */
  15. @Override
  16. protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
  17. boolean allowed = false;
  18. try {
  19. allowed = executeLogin(request, response);
  20. } catch(IllegalStateException e){ //not found any token
  21. System.out.println("Not found any token");
  22. }catch (Exception e) {
  23. System.out.println("Error occurs when login");
  24. }
  25. return allowed || super.isPermissive(mappedValue);
  26. }
  27. /**
  28. * 父类executeLogin()首先会createToken(),然后调用shiro的Subject.login()方法。
  29. *
  30. * executeLogin()的逻辑是不是跟UserController里面的密码登录逻辑很像?
  31. *
  32. * @param request
  33. * @param response
  34. * @return
  35. */
  36. @Override
  37. protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
  38. HttpServletRequest httpRequest = (HttpServletRequest) request;
  39. // 从请求头中的Authorization字段尝试获取jwt token
  40. String token = httpRequest.getHeader("Authorization");
  41. if (StringUtils.isEmpty(token)) {
  42. // 从请求头中的token字段(自定义字段)尝试获取jwt token
  43. token = httpRequest.getHeader("token");
  44. }
  45. if (StringUtils.isEmpty(token)) {
  46. // 从url参数中尝试获取jwt token
  47. token = httpRequest.getParameter("token");
  48. }
  49. if (!StringUtils.isEmpty(token)) {
  50. return new JwtToken(token);
  51. }
  52. return null;
  53. }
  54. /**
  55. * 如果这个Filter在之前isAccessAllowed()方法中返回false,则会进入这个方法。我们这里直接返回错误的response
  56. * @param request
  57. * @param response
  58. * @return
  59. * @throws Exception
  60. */
  61. @Override
  62. protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
  63. HttpServletResponse httpResponse = WebUtils.toHttp(response);
  64. httpResponse.setCharacterEncoding("UTF-8");
  65. httpResponse.setContentType("application/json;charset=UTF-8");
  66. httpResponse.setStatus(HttpStatus.NON_AUTHORITATIVE_INFORMATION.value());
  67. PrintWriter writer = response.getWriter();
  68. writer.print("无效token");
  69. fillCorsHeader(request, httpResponse);
  70. return false;
  71. }
  72. /**
  73. * 登录成功后判断是否需要刷新token
  74. * 登录成功说明:jwt有效,尚未过期。当离过期时间不足一天时,往响应头中放入新的token返回给前端
  75. *
  76. * @param token
  77. * @param subject
  78. * @param request
  79. * @param response
  80. * @return
  81. */
  82. @Override
  83. protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
  84. ServletRequest request, ServletResponse response) {
  85. String oldToken = (String) token.getPrincipal();
  86. Date expireAt = JwtUtil.getExpireAt(oldToken);
  87. int countDownDays = (int) DateTimeUtil.differDaysBetween(
  88. LocalDateTime.now(), DateTimeUtil.toLocalDateTime(expireAt));
  89. if (shouldRefreshToken && !ObjectUtils.isEmpty(expireAt)
  90. && countDownDays < 1) { // 如果离过期时间不足一天
  91. UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
  92. User user = (User) subject.getPrincipal();
  93. String newToken = userService.generateJwt(user.getUsername());
  94. HttpServletResponse httpResponse = (HttpServletResponse) response;
  95. httpResponse.addHeader("token", newToken);
  96. }
  97. return true;
  98. }
  99. /**
  100. * 添加跨域支持
  101. * @param request
  102. * @param response
  103. * @throws Exception
  104. */
  105. @Override
  106. protected void postHandle(ServletRequest request, ServletResponse response) {
  107. fillCorsHeader(request, response);
  108. }
  109. /**
  110. * 设置跨域
  111. */
  112. public void fillCorsHeader(ServletRequest request, ServletResponse response) {
  113. HttpServletRequest httpRequest = (HttpServletRequest) request;
  114. HttpServletResponse httpResponse = (HttpServletResponse) response;
  115. httpResponse.setHeader("Access-control-Allow-Origin", httpRequest.getHeader("Origin"));
  116. httpResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
  117. httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers"));
  118. // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
  119. if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
  120. httpResponse.setStatus(HttpStatus.OK.value());
  121. }
  122. }
  123. public boolean isShouldRefreshToken() {
  124. return shouldRefreshToken;
  125. }
  126. public void setShouldRefreshToken(boolean shouldRefreshToken) {
  127. this.shouldRefreshToken = shouldRefreshToken;
  128. }
  129. }

JwtRealm

  1. /**
  2. * @author passerbyYSQ
  3. * @create 2020-08-23 18:24
  4. */
  5. public class JwtRealm extends AuthorizingRealm {
  6. public JwtRealm() {
  7. // 用我们自定的Matcher
  8. this.setCredentialsMatcher(new JwtCredentialsMatcher());
  9. }
  10. @Override
  11. public boolean supports(AuthenticationToken token) {
  12. return token instanceof JwtToken;
  13. }
  14. @Override
  15. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  16. return null;
  17. }
  18. @Override
  19. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  20. // JwtToken jwtToken = (JwtToken) token;
  21. // String tokenStr = jwtToken.getToken();
  22. // 取决于JwtToken的getPrincipal()
  23. String tokenStr = (String) token.getPrincipal();
  24. // 从jwt字符串中解析出username信息
  25. String username = JwtUtil.getClaimByKey(tokenStr, "username");
  26. if (!Strings.isEmpty(username)) {
  27. UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
  28. // 根据token中的username去数据库核对信息,返回用户信息,并封装称SimpleAuthenticationInfo给Matcher去校验
  29. User user = userService.findByUsername(username);
  30. // principle是身份信息,简单的可以放username,也可以将User对象作为身份信息
  31. // 身份信息可以在登录成功之后通过subject.getPrinciple()取出
  32. return new SimpleAuthenticationInfo(user, user.getJwtSecret(), this.getName());
  33. }
  34. return null;
  35. }
  36. }

JwtCredentialsMatcher:Jwt的比较器

  1. /**
  2. * @author passerbyYSQ
  3. * @create 2020-08-23 18:42
  4. */
  5. public class JwtCredentialsMatcher implements CredentialsMatcher {
  6. @Override
  7. public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
  8. // AuthenticationInfo info 是我们在JwtRealm中doGetAuthenticationInfo()返回的那个
  9. User user = (User) info.getPrincipals().getPrimaryPrincipal();
  10. String secret = (String) info.getCredentials();
  11. // String tokenStr = ((JwtToken) token).getToken();
  12. String tokenStr = (String) token.getPrincipal();
  13. // 校验jwt有效
  14. return JwtUtil.verifyJwt(tokenStr, user.getUsername(), secret);
  15. }
  16. }

五、Shiro的自定义配置类

总结来说,这个配置类主要干两种事情:

1、将我们上述定义的Realm,过滤器等,注册给Shiro

2、设置请求的拦截规则

代码解析细节,参看注释。个人强迫症,注释写的还算是比较可以的。

  1. /**
  2. * 整合Shiro框架的配置类
  3. *
  4. * @author passerbyYSQ
  5. * @create 2020-08-20 23:10
  6. */
  7. @Configuration
  8. public class ShiroConfig {
  9. @Bean
  10. public ShiroFilterFactoryBean shiroFilterFactoryBean(
  11. DefaultWebSecurityManager securityManager, ShiroFilterChainDefinition chainDefinition) {
  12. ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
  13. // 必须的设置。我们自定义的Realm此时已经被设置到securityManager中了
  14. factoryBean.setSecurityManager(securityManager);
  15. // 注册我们写的过滤器
  16. Map<String, Filter> filters = factoryBean.getFilters();
  17. filters.put("jwtAuth", new JwtAuthenticatingFilter());
  18. factoryBean.setFilters(filters);
  19. // 设置请求的过滤规则。其中过滤规则中用到了我们注册的过滤器:jwtAuth
  20. factoryBean.setFilterChainDefinitionMap(chainDefinition.getFilterChainMap());
  21. return factoryBean;
  22. }
  23. @Bean
  24. public DefaultWebSecurityManager securityManager(Authenticator authenticator) {
  25. DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
  26. // 所有的Realm都用这个全局缓存。不生效,需要在realm中设置缓存。原因暂时搞不懂。
  27. // securityManager.setCacheManager(new EhCacheManager());
  28. securityManager.setAuthenticator(authenticator);
  29. return securityManager;
  30. }
  31. /**
  32. * 设置请求的过滤规则
  33. * @return
  34. */
  35. @Bean
  36. public ShiroFilterChainDefinition shiroFilterChainDefinition() {
  37. DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
  38. chainDefinition.addPathDefinition("/user/register", "noSessionCreation,anon");
  39. chainDefinition.addPathDefinition("/user/login", "noSessionCreation,anon"); //login不做认证,noSessionCreation的作用是用户在操作session时会抛异常
  40. // 注意第2个参数的"jwtAuth"需要与上面的 filters.put("jwtAuth", new JwtAuthenticatingFilter()); 一致
  41. chainDefinition.addPathDefinition("/user/logout", "noSessionCreation,jwtAuth[permissive]"); //做用户认证,permissive参数的作用是当token无效时也允许请求访问,不会返回鉴权未通过的错误
  42. chainDefinition.addPathDefinition("/**", "noSessionCreation,jwtAuth"); // 默认进行用户鉴权
  43. return chainDefinition;
  44. }
  45. /**
  46. * 初始化Authenticator,将我们需要的Realm设置进去
  47. * Shiro会将Authenticator设置到SecurityManager里面
  48. */
  49. @Bean
  50. public Authenticator authenticator(@Qualifier("loginRealm") Realm loginRealm, @Qualifier("jwtRealm") Realm jwtRealm) {
  51. ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
  52. //设置两个Realm,一个用于用户登录验证和访问权限获取;一个用于jwt token的认证
  53. authenticator.setRealms(Arrays.asList(loginRealm, jwtRealm));
  54. //设置多个realm认证策略,一个成功即跳过其它的
  55. authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
  56. return authenticator;
  57. }
  58. /**
  59. * 返回我们自定义的Realm
  60. *
  61. * @return
  62. */
  63. @Bean("loginRealm") // 自动配置类中有同名组件,如果只写@Bean,会出现歧义
  64. public Realm loginRealm(EhCacheManager ehCacheManager) {
  65. LoginRealm loginRealm = new LoginRealm();
  66. // AuthenticatingRealm里面的isAuthenticationCachingEnabled()
  67. loginRealm.setCacheManager(ehCacheManager);
  68. loginRealm.setCachingEnabled(true); // 这句话不能少!!!
  69. loginRealm.setAuthenticationCachingEnabled(true); // 认证缓存
  70. loginRealm.setAuthorizationCachingEnabled(true); // 授权缓存
  71. return loginRealm;
  72. }
  73. @Bean("jwtRealm")
  74. public Realm jwtRealm(EhCacheManager ehCacheManager) {
  75. JwtRealm jwtRealm = new JwtRealm();
  76. jwtRealm.setCacheManager(ehCacheManager);
  77. jwtRealm.setCachingEnabled(true); // 这句话不能少!!!
  78. jwtRealm.setAuthenticationCachingEnabled(true); // 认证缓存
  79. jwtRealm.setAuthorizationCachingEnabled(true); // 授权缓存
  80. return jwtRealm;
  81. }
  82. /**
  83. * 禁用session, 不保存用户登录状态。保证每次请求都重新认证。
  84. * 需要注意的是,如果用户代码里调用Subject.getSession()还是可以用session,
  85. * 如果要完全禁用,要配合上过滤规则的noSessionCreation的Filter来实现
  86. */
  87. @Bean
  88. protected SessionStorageEvaluator sessionStorageEvaluator(){
  89. DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
  90. sessionStorageEvaluator.setSessionStorageEnabled(false);
  91. return sessionStorageEvaluator;
  92. }
  93. /**
  94. * shiro的全局缓存管理器
  95. * @return
  96. */
  97. @Bean
  98. public EhCacheManager ehCacheManager() {
  99. return new EhCacheManager();
  100. }
  101. }

博客注重思路逻辑分析。有需要的同学可以去Github自取源码。GitHub - passerbyYSQ/SpringBoot_Shiro_Jwt

求赞和收藏。。。么么哒

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/157238
推荐阅读
相关标签
  

闽ICP备14008679号