分享

spring boot2整合shiro安全框架实现前后端分离的JWT token登录验证

 忠波irlphwt1ng 2019-12-01

代码略多,粘贴一些关键的代码,完整demo当然必须放在GitHub上面啦,当然带SQL文件的,在项目里面

GitHub地址:  https://github.com/zhang-xiaoxiang/shiro-jwt

说明:由于初衷是解决自己项目的bug的,就找的网上的一面博客瞎搞了一个demo.然后报的错网上难以找到解决办法,后来自己解决了,就记录一下,所以不算教程,我看评论大伙说的修改密码后之前的token仍然有效,可以使用redis处理,或者其他业务逻辑代码处理一下,这个仅仅是一个demo,当然很多培训机构的源码也有一些demo,大家可以参考,这里主要是给需要的小伙伴一个思路或者遇到那个bug的解决方案

关于这个例子介绍:

1使用的是spring boot2作为环境基础

2数据库框架是mybatis plus(对mybatis熟悉就行,你顺便看一下mybatis plus逆向生成代码的魅力)

3包含全局异常的处理(报错getWriter() has already been called for this response),这个BUG是网上的访问量比较高的CSDN的博客案例留下的bug,为了解决这个bug才有我的这篇博客,所以记录一下

4shiro可以实现除了验证和授权之外的一些功能,这里主要展示验证,验证相当于登录,授权相当于登录后查看是否有权限,就想即使登录了支付宝,但是付款的时候还是需要验证支付宝密码一样,当然前后端未分离可以使用thymeleaf网页引擎,貌似黑马视频有,这里就只做个登录例子

5jwt这种方式是不太时尚的,正确的方式spring security oauth2实现认证授权,这里仅供参考,欢迎小伙伴提出宝贵意见,我都会尽快回复

6使用的是mysql8,pom和properties文件你改成现在用得多一点的5.7版本的写法就行了

7这里根据博友评论确实忘了用户修改密码token的处理,我相信这个大家应该会处理的(更新一下token就行了)

主要步骤:

首先是pom引入(主要的依赖,由于代码较大,而且我提供了Git地址,需要的直接从GitHub拉下来就行了,后面不做过多阐述)

  1. <!--JWT java web token 权限验证-->
  2. <dependency>
  3. <groupId>com.auth0</groupId>
  4. <artifactId>java-jwt</artifactId>
  5. <version>3.4.0</version>
  6. </dependency>
  7. <!--shiro 权限框架-->
  8. <dependency>
  9. <groupId>org.apache.shiro</groupId>
  10. <artifactId>shiro-spring</artifactId>
  11. <version>1.4.0</version>
  12. </dependency>

 

实现shiro的AuthenticationToken接口的类JwtToken
  1. //package com.example.shirojwt.jwt;
  2. import org.apache.shiro.authc.AuthenticationToken;
  3. /**
  4. * JwtToken:实现shiro的AuthenticationToken接口的类JwtToken
  5. *
  6. * @author zhangxiaoxiang
  7. * @date: 2019/07/12
  8. */
  9. public class JwtToken implements AuthenticationToken{
  10. private String token;
  11. public JwtToken(String token) {
  12. this.token = token;
  13. }
  14. @Override
  15. public Object getPrincipal() {
  16. return token;
  17. }
  18. @Override
  19. public Object getCredentials() {
  20. return token;
  21. }
  22. }

 再写一个token的验证工具类

  1. //package com.example.shirojwt.util;
  2. import com.auth0.jwt.JWT;
  3. import com.auth0.jwt.JWTVerifier;
  4. import com.auth0.jwt.algorithms.Algorithm;
  5. import com.auth0.jwt.exceptions.JWTDecodeException;
  6. import com.auth0.jwt.interfaces.DecodedJWT;
  7. import lombok.extern.slf4j.Slf4j;
  8. import java.util.Date;
  9. /**
  10. * JwtUtil:用来进行签名和效验Token
  11. *
  12. * @author zhangxiaoxiang
  13. * @date: 2019/07/12
  14. */
  15. @Slf4j
  16. public class JwtUtil {
  17. /**
  18. * JWT验证过期时间 EXPIRE_TIME 分钟
  19. */
  20. private static final long EXPIRE_TIME = 30 * 60 * 1000;
  21. /**
  22. * 校验token是否正确
  23. *
  24. * @param token 密钥
  25. * @param secret 用户的密码
  26. * @return 是否正确
  27. */
  28. public static boolean verify(String token, String username, String secret) {
  29. try {
  30. //根据密码生成JWT效验器
  31. Algorithm algorithm = Algorithm.HMAC256(secret);
  32. JWTVerifier verifier = JWT.require(algorithm)
  33. .withClaim("username", username)
  34. .build();
  35. //效验TOKEN
  36. DecodedJWT jwt = verifier.verify(token);
  37. log.info("登录验证成功!");
  38. return true;
  39. } catch (Exception exception) {
  40. log.error("JwtUtil登录验证失败!");
  41. return false;
  42. }
  43. }
  44. /**
  45. * 获得token中的信息无需secret解密也能获得
  46. *
  47. * @return token中包含的用户名
  48. */
  49. public static String getUsername(String token) {
  50. try {
  51. DecodedJWT jwt = JWT.decode(token);
  52. return jwt.getClaim("username").asString();
  53. } catch (JWTDecodeException e) {
  54. return null;
  55. }
  56. }
  57. /**
  58. * 生成token签名EXPIRE_TIME 分钟后过期
  59. *
  60. * @param username 用户名(电话号码)
  61. * @param secret 用户的密码
  62. * @return 加密的token
  63. */
  64. public static String sign(String username, String secret) {
  65. Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
  66. Algorithm algorithm = Algorithm.HMAC256(secret);
  67. // 附带username信息
  68. return JWT.create()
  69. .withClaim("username", username)
  70. .withExpiresAt(date)
  71. .sign(algorithm);
  72. }
  73. public static void main(String[] args) {
  74. /**
  75. * 测试生成一个token
  76. */
  77. String sign = sign("18888888888", "123456");
  78. log.warn("测试生成一个token\n"+sign);
  79. }
  80. }

写一个jwt过滤器来作为shiro的过滤器。

  1. //package com.example.shirojwt.filter;
  2. import com.alibaba.fastjson.JSONObject;
  3. import com.alibaba.fastjson.serializer.SerializerFeature;
  4. import com.example.shirojwt.jwt.JwtToken;
  5. import com.example.shirojwt.result.ResponseData;
  6. import com.example.shirojwt.result.ResponseDataUtil;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.apache.shiro.authc.AuthenticationException;
  9. import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
  10. import org.springframework.http.HttpStatus;
  11. import org.springframework.stereotype.Component;
  12. import org.springframework.web.bind.annotation.RequestMethod;
  13. import javax.servlet.*;
  14. import javax.servlet.http.HttpServletRequest;
  15. import javax.servlet.http.HttpServletResponse;
  16. /**
  17. * JwtFilter:jwt过滤器来作为shiro的过滤器
  18. *
  19. * @author zhangxiaoxiang
  20. * @date: 2019/07/12
  21. */
  22. @Slf4j
  23. @Component//这个注入与否影响不大
  24. public class JwtFilter extends BasicHttpAuthenticationFilter implements Filter {
  25. /**
  26. * 执行登录
  27. * @param request
  28. * @param response
  29. * @return
  30. * @throws Exception
  31. */
  32. @Override
  33. protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
  34. HttpServletRequest httpServletRequest = (HttpServletRequest) request;
  35. String token = httpServletRequest.getHeader("Token");
  36. JwtToken jwtToken = new JwtToken(token);
  37. // 提交给realm进行登入,如果错误他会抛出异常并被捕获
  38. try {
  39. getSubject(request, response).login(jwtToken);
  40. // 如果没有抛出异常则代表登入成功,返回true
  41. return true;
  42. } catch (AuthenticationException e) {
  43. ResponseData responseData = ResponseDataUtil.authorizationFailed( "没有访问权限,原因是:" + e.getMessage());
  44. //SerializerFeature.WriteMapNullValue为了null属性也输出json的键值对
  45. Object o = JSONObject.toJSONString(responseData, SerializerFeature.WriteMapNullValue);
  46. response.setCharacterEncoding("utf-8");
  47. response.getWriter().print(o);
  48. return false;
  49. }
  50. }
  51. /**
  52. * 执行登录认证
  53. *
  54. * @param request
  55. * @param response
  56. * @param mappedValue
  57. * @return
  58. */
  59. @Override
  60. protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
  61. try {
  62. return executeLogin(request, response);
  63. // return true;有一篇博客这里直接返回true是不正确的,在这里我特别指出一下
  64. } catch (Exception e) {
  65. log.error("JwtFilter过滤验证失败!");
  66. return false;
  67. }
  68. }
  69. /**
  70. * 对跨域提供支持
  71. * @param request
  72. * @param response
  73. * @return
  74. * @throws Exception
  75. */
  76. @Override
  77. protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
  78. HttpServletRequest httpServletRequest = (HttpServletRequest) request;
  79. HttpServletResponse httpServletResponse = (HttpServletResponse) response;
  80. httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
  81. httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
  82. httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
  83. // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
  84. if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
  85. httpServletResponse.setStatus(HttpStatus.OK.value());
  86. return false;
  87. }
  88. return super.preHandle(request, response);
  89. }
  90. }

自定义Realm

  1. //package com.example.shirojwt.shiro;
  2. import com.example.shirojwt.entity.User;
  3. import com.example.shirojwt.jwt.JwtToken;
  4. import com.example.shirojwt.service.UserService;
  5. import com.example.shirojwt.util.JwtUtil;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.apache.shiro.authc.AuthenticationException;
  8. import org.apache.shiro.authc.AuthenticationInfo;
  9. import org.apache.shiro.authc.AuthenticationToken;
  10. import org.apache.shiro.authc.SimpleAuthenticationInfo;
  11. import org.apache.shiro.authz.AuthorizationInfo;
  12. import org.apache.shiro.authz.SimpleAuthorizationInfo;
  13. import org.apache.shiro.realm.AuthorizingRealm;
  14. import org.apache.shiro.subject.PrincipalCollection;
  15. import org.springframework.beans.factory.annotation.Autowired;
  16. import org.springframework.stereotype.Component;
  17. /**
  18. * MyRealm:自定义一个授权
  19. *
  20. * @author zhangxiaoxiang
  21. * @date: 2019/07/12
  22. */
  23. @Component
  24. @Slf4j
  25. public class MyRealm extends AuthorizingRealm {
  26. @Autowired
  27. private UserService userService;
  28. /**
  29. * 必须重写此方法,不然Shiro会报错
  30. * @param token
  31. * @return
  32. */
  33. @Override
  34. public boolean supports(AuthenticationToken token) {
  35. return token instanceof JwtToken;
  36. }
  37. /**
  38. * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
  39. * @param principals
  40. * @return
  41. */
  42. @Override
  43. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  44. String username = JwtUtil.getUsername(principals.toString());
  45. User user = userService.findByUserName(username);
  46. SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
  47. return simpleAuthorizationInfo;
  48. }
  49. /**
  50. * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
  51. * @param auth
  52. * @return
  53. * @throws AuthenticationException
  54. */
  55. @Override
  56. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
  57. String token = (String) auth.getCredentials();
  58. // 解密获得username,用于和数据库进行对比
  59. String username = null;
  60. try {
  61. //这里工具类没有处理空指针等异常这里处理一下(这里处理科学一些)
  62. username = JwtUtil.getUsername(token);
  63. } catch (Exception e) {
  64. throw new AuthenticationException("heard的token拼写错误或者值为空");
  65. }
  66. if (username == null) {
  67. log.error("token无效(空''或者null都不行!)");
  68. throw new AuthenticationException("token无效");
  69. }
  70. User userBean = userService.findByUserName(username);
  71. if (userBean == null) {
  72. log.error("用户不存在!)");
  73. throw new AuthenticationException("用户不存在!");
  74. }
  75. if (!JwtUtil.verify(token, username, userBean.getUserPassword())) {
  76. log.error("用户名或密码错误(token无效或者与登录者不匹配)!)");
  77. throw new AuthenticationException("用户名或密码错误(token无效或者与登录者不匹配)!");
  78. }
  79. return new SimpleAuthenticationInfo(token, token, "my_realm");
  80. }
  81. }

 ShiroConfig核心配置

  1. //package com.example.shirojwt.config;
  2. import com.example.shirojwt.filter.JwtFilter;
  3. import com.example.shirojwt.shiro.MyRealm;
  4. import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
  5. import org.apache.shiro.mgt.DefaultSubjectDAO;
  6. import org.apache.shiro.mgt.SecurityManager;
  7. import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
  8. import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
  9. import org.springframework.context.annotation.Bean;
  10. import org.springframework.context.annotation.Configuration;
  11. import javax.servlet.Filter;
  12. import java.util.HashMap;
  13. import java.util.LinkedHashMap;
  14. import java.util.Map;
  15. /**
  16. * ShiroConfig:shiro 配置类,配置哪些拦截,哪些不拦截,哪些授权等等各种配置都在这里
  17. *
  18. * 很多都是老套路,按照这个套路配置就行了
  19. *
  20. * @author zhangxiaoxiang
  21. * @date: 2019/07/12
  22. */
  23. @Configuration
  24. public class ShiroConfig {
  25. /**
  26. * 注入安全过滤器
  27. * @param securityManager
  28. * @return
  29. */
  30. @Bean("shiroFilter")
  31. public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
  32. ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
  33. shiroFilterFactoryBean.setSecurityManager(securityManager);
  34. //拦截器
  35. Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
  36. // 配置不会被拦截的链接 顺序判断
  37. filterChainDefinitionMap.put("/login/**", "anon");
  38. filterChainDefinitionMap.put("/**.js", "anon");
  39. filterChainDefinitionMap.put("/druid/**", "anon");
  40. filterChainDefinitionMap.put("/swagger**/**", "anon");
  41. filterChainDefinitionMap.put("/**/swagger**/**", "anon");
  42. filterChainDefinitionMap.put("/webjars/**", "anon");
  43. filterChainDefinitionMap.put("/v2/**", "anon");
  44. // 添加自己的过滤器并且取名为jwt
  45. Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
  46. filterMap.put("jwt", new JwtFilter());
  47. shiroFilterFactoryBean.setFilters(filterMap);
  48. //<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
  49. filterChainDefinitionMap.put("/**", "jwt");
  50. //未授权界面;
  51. shiroFilterFactoryBean.setUnauthorizedUrl("/403");
  52. shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
  53. return shiroFilterFactoryBean;
  54. }
  55. /**
  56. * 注入安全管理器
  57. * @param myRealm
  58. * @return
  59. */
  60. @Bean("securityManager")
  61. public SecurityManager securityManager(MyRealm myRealm) {
  62. DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
  63. securityManager.setRealm(myRealm);
  64. /*
  65. * 关闭shiro自带的session,详情见文档
  66. * http://shiro./session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
  67. */
  68. DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
  69. DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
  70. defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
  71. subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
  72. securityManager.setSubjectDAO(subjectDAO);
  73. return securityManager;
  74. }
  75. }

 大体核心代码已经完成,项目结构如图(点击图片放大后就清晰了)

 测试示例

比如模拟登录token(在JwtUtil生成一个)错误,或者token输入成token12,或者账号密码错误等等...

 比如输入使用生成的token(相当于用户登录后返回的token):登录显然不需要token,登录后后台返回token,只是这里为了演示一个需要token的接口,所以把登录接口也设置了需要token,科学的方法是比如查询用户详情这个接口就必须要token,所以postman传入的token模拟的是需要token的接口而存在的,返回的token是给前端带上访问用户订单,地址的那些需要token认证的接口

 

 当然在控制层也可以测试其他被限制访问的接口和放行的接口,以及各种异常的测试,这里不做过多赘述,感兴趣的看GitHub的demo

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多