通过笔者前两篇文章的说明,相信大家已经知道JWT是什么,怎么用,该如何结合Spring Security使用。那么本节就用代码来具体的实现一下JWT登录认证及鉴权的流程。

一、环境准备工作

  • 建立Spring Boot项目并集成了Spring Security,项目可以正常启动
  • 通过controller写一个HTTP的GET方法服务接口,比如:“/hello”
  • 实现最基本的动态数据验证及权限分配,即实现UserDetailsService接口和UserDetails接口。这两个接口都是向Spring Security提供用户、角色、权限等校验信息的接口
  • 如果你学习过Spring Security的formLogin登录模式,请将HttpSecurity配置中的formLogin()配置段全部去掉。因为JWT完全使用JSON接口,没有from表单提交。
  • HttpSecurity配置中一定要加上csrf().disable(),即暂时关掉跨站攻击CSRF的防御。这样是不安全的,我们后续章节再做处理。

以上的内容,我们在之前的文章中都已经讲过。如果仍然不熟悉,可以翻看本号之前的文章。

## 二、开发JWT工具类
通过maven坐标引入JWT工具包jjwt

  1. <dependency>
  2. <groupId>io.jsonwebtoken</groupId>
  3. <artifactId>jjwt</artifactId>
  4. <version>0.9.0</version>
  5. </dependency>

在application.yml中加入如下自定义一些关于JWT的配置

  1. jwt:
  2. header: JWTHeaderName
  3. secret: aabbccdd
  4. expiration: 3600000
  • 其中header是携带JWT令牌的HTTP的Header的名称。虽然我这里叫做JWTHeaderName,但是在实际生产中可读性越差越安全。
  • secret是用来为JWT基础信息加密和解密的密钥。虽然我在这里在配置文件写死了,但是在实际生产中通常不直接写在配置文件里面。而是通过应用的启动参数传递,并且需要定期修改。
  • expiration是JWT令牌的有效时间。

写一个Spring Boot配置自动加载的工具类。

  1. @Data
  2. @ConfigurationProperties(prefix = "jwt") //配置自动加载,prefix是配置的前缀
  3. @Component
  4. public class JwtTokenUtil implements Serializable {
  5. private String secret;
  6. private Long expiration;
  7. private String header;
  8. /**
  9. * 生成token令牌
  10. *
  11. * @param userDetails 用户
  12. * @return 令token牌
  13. */
  14. public String generateToken(UserDetails userDetails) {
  15. Map<String, Object> claims = new HashMap<>(2);
  16. claims.put("sub", userDetails.getUsername());
  17. claims.put("created", new Date());
  18. return generateToken(claims);
  19. }
  20. /**
  21. * 从令牌中获取用户名
  22. *
  23. * @param token 令牌
  24. * @return 用户名
  25. */
  26. public String getUsernameFromToken(String token) {
  27. String username;
  28. try {
  29. Claims claims = getClaimsFromToken(token);
  30. username = claims.getSubject();
  31. } catch (Exception e) {
  32. username = null;
  33. }
  34. return username;
  35. }
  36. /**
  37. * 判断令牌是否过期
  38. *
  39. * @param token 令牌
  40. * @return 是否过期
  41. */
  42. public Boolean isTokenExpired(String token) {
  43. try {
  44. Claims claims = getClaimsFromToken(token);
  45. Date expiration = claims.getExpiration();
  46. return expiration.before(new Date());
  47. } catch (Exception e) {
  48. return false;
  49. }
  50. }
  51. /**
  52. * 刷新令牌
  53. *
  54. * @param token 原令牌
  55. * @return 新令牌
  56. */
  57. public String refreshToken(String token) {
  58. String refreshedToken;
  59. try {
  60. Claims claims = getClaimsFromToken(token);
  61. claims.put("created", new Date());
  62. refreshedToken = generateToken(claims);
  63. } catch (Exception e) {
  64. refreshedToken = null;
  65. }
  66. return refreshedToken;
  67. }
  68. /**
  69. * 验证令牌
  70. *
  71. * @param token 令牌
  72. * @param userDetails 用户
  73. * @return 是否有效
  74. */
  75. public Boolean validateToken(String token, UserDetails userDetails) {
  76. SysUser user = (SysUser) userDetails;
  77. String username = getUsernameFromToken(token);
  78. return (username.equals(user.getUsername()) && !isTokenExpired(token));
  79. }
  80. /**
  81. * 从claims生成令牌,如果看不懂就看谁调用它
  82. *
  83. * @param claims 数据声明
  84. * @return 令牌
  85. */
  86. private String generateToken(Map<String, Object> claims) {
  87. Date expirationDate = new Date(System.currentTimeMillis() + expiration);
  88. return Jwts.builder().setClaims(claims)
  89. .setExpiration(expirationDate)
  90. .signWith(SignatureAlgorithm.HS512, secret)
  91. .compact();
  92. }
  93. /**
  94. * 从令牌中获取数据声明,如果看不懂就看谁调用它
  95. *
  96. * @param token 令牌
  97. * @return 数据声明
  98. */
  99. private Claims getClaimsFromToken(String token) {
  100. Claims claims;
  101. try {
  102. claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
  103. } catch (Exception e) {
  104. claims = null;
  105. }
  106. return claims;
  107. }
  108. }

上面的代码就是使用io.jsonwebtoken.jjwt提供的方法开发JWT令牌生成、刷新的工具类。

三、开发登录接口(获取Token的接口)

  • "/authentication"接口用于登录验证,并且生成JWT返回给客户端
  • "/refreshtoken"接口用于刷新JWT,更新JWT令牌的有效期
  1. @RestController
  2. public class JwtAuthController {
  3. @Resource
  4. private JwtAuthService jwtAuthService;
  5. @PostMapping(value = "/authentication")
  6. public AjaxResponse login(@RequestBody Map<String, String> map) {
  7. String username = map.get("username");
  8. String password = map.get("password");
  9. if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
  10. return AjaxResponse.error(
  11. new CustomException(CustomExceptionType.USER_INPUT_ERROR,"用户名密码不能为空"));
  12. }
  13. return AjaxResponse.success(jwtAuthService.login(username, password));
  14. }
  15. @PostMapping(value = "/refreshtoken")
  16. public AjaxResponse refresh(@RequestHeader("${jwt.header}") String token) {
  17. return AjaxResponse.success(jwtAuthService.refreshToken(token));
  18. }
  19. }

核心的token业务逻辑写在JwtAuthService 中

  • login方法中首先使用用户名、密码进行登录验证。如果验证失败抛出BadCredentialsException异常。如果验证成功,程序继续向下走,生成JWT响应给前端
  • refreshToken方法只有在JWT token没有过期的情况下才能刷新,过期了就不能刷新了。需要重新登录。
  1. @Service
  2. public class JwtAuthService {
  3. @Resource
  4. private AuthenticationManager authenticationManager;
  5. @Resource
  6. private UserDetailsService userDetailsService;
  7. @Resource
  8. private JwtTokenUtil jwtTokenUtil;
  9. public String login(String username, String password) {
  10. //使用用户名密码进行登录验证
  11. UsernamePasswordAuthenticationToken upToken =
  12. new UsernamePasswordAuthenticationToken( username, password );
  13. Authentication authentication = authenticationManager.authenticate(upToken);
  14. SecurityContextHolder.getContext().setAuthentication(authentication);
  15. //生成JWT
  16. UserDetails userDetails = userDetailsService.loadUserByUsername( username );
  17. return jwtTokenUtil.generateToken(userDetails);
  18. }
  19. public String refreshToken(String oldToken) {
  20. if (!jwtTokenUtil.isTokenExpired(oldToken)) {
  21. return jwtTokenUtil.refreshToken(oldToken);
  22. }
  23. return null;
  24. }
  25. }

因为使用到了AuthenticationManager ,所以在继承WebSecurityConfigurerAdapter的SpringSecurity配置实现类中,将AuthenticationManager 声明为一个Bean。并将"/authentication"和 "/refreshtoken"开放访问权限,如何开放访问权限,我们之前的文章已经讲过了。

  1. @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
  2. @Override
  3. public AuthenticationManager authenticationManagerBean() throws Exception {
  4. return super.authenticationManagerBean();
  5. }

四、接口访问鉴权过滤器

当用户第一次登陆之后,我们将JWT令牌返回给了客户端,客户端应该将该令牌保存起来。在进行接口请求的时候,将令牌带上,放到HTTP的header里面,header的名字要和jwt.header的配置一致,这样服务端才能解析到。下面我们定义一个拦截器:

  • 拦截接口请求,从请求request获取token,从token中解析得到用户名
  • 然后通过UserDetailsService获得系统用户(从数据库、或其他其存储介质)
  • 根据用户信息和JWT令牌,验证系统用户与用户输入的一致性,并判断JWT是否过期。如果没有过期,至此表明了该用户的确是该系统的用户。
  • 但是,你是系统用户不代表你可以访问所有的接口。所以需要构造UsernamePasswordAuthenticationToken传递用户、权限信息,并将这些信息通过authentication告知Spring Security。Spring Security会以此判断你的接口访问权限。
  1. @Slf4j
  2. @Component
  3. public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
  4. @Resource
  5. private MyUserDetailsService userDetailsService;
  6. @Resource
  7. private JwtTokenUtil jwtTokenUtil;
  8. @Override
  9. protected void doFilterInternal(HttpServletRequest request,
  10. HttpServletResponse response,
  11. FilterChain chain) throws ServletException, IOException {
  12. // 从这里开始获取 request 中的 jwt token
  13. String authHeader = request.getHeader(jwtTokenUtil.getHeader());
  14. log.info("authHeader:{}", authHeader);
  15. // 验证token是否存在
  16. if (authHeader != null && StringUtils.isNotEmpty(authHeader)) {
  17. // 根据token 获取用户名
  18. String username = jwtTokenUtil.getUsernameFromToken(authHeader);
  19. if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
  20. // 通过用户名 获取用户的信息
  21. UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
  22. // 验证JWT是否过期
  23. if (jwtTokenUtil.validateToken(authHeader, userDetails)) {
  24. //加载用户、角色、权限信息,Spring Security根据这些信息判断接口的访问权限
  25. UsernamePasswordAuthenticationToken authentication
  26. = new UsernamePasswordAuthenticationToken(userDetails, null,
  27. userDetails.getAuthorities());
  28. authentication.setDetails(new WebAuthenticationDetailsSource()
  29. .buildDetails(request));
  30. SecurityContextHolder.getContext().setAuthentication(authentication);
  31. }
  32. }
  33. }
  34. chain.doFilter(request, response);
  35. }
  36. }

在spring Security的配置类(即WebSecurityConfigurerAdapter实现类的configure(HttpSecurity http)配置方法中,加入如下配置:

  1. .sessionManagement()
  2. .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
  3. .and()
  4. .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
  • 因为我们使用了JWT,表明了我们的应用是一个前后端分离的应用,所以我们可以开启STATELESS禁止使用session。当然这并不绝对,前后端分离的应用通过一些办法也是可以使用session的,这不是本文的核心内容不做赘述。
  • 将我们的自定义jwtAuthenticationTokenFilter,加载到UsernamePasswordAuthenticationFilter的前面。

五、测试一下:

测试登录接口,即:获取token的接口。输入正确的用户名、密码即可获取token。

下面我们访问一个我们定义的简单的接口“/hello”,但是不传递JWT令牌,结果是禁止访问。当我们将上一步返回的token,传递到header中,就能正常响应hello的接口结果。

期待您的关注

SpringSecurity代码实现JWT接口权限授予与校验的更多相关文章

  1. 【JEECG技术文档】JEECG 接口权限开发及配置使用说明

    1.功能介绍   通过接口配置实现,对接口的访问权限控制和数据权限控制,接口时REST接口,接口权限认证机制使用Json web token (JWT) 接口权限调用流程: (1)通过接口用户的用户名 ...

  2. SpringBoot系列 - 集成JWT实现接口权限认证

    会飞的污熊 2018-01-22 16173 阅读 spring jwt springboot RESTful API认证方式 一般来讲,对于RESTful API都会有认证(Authenticati ...

  3. SpringBoot+Shiro+JWT前后端分离实现用户权限和接口权限控制

    1. 引入需要的依赖 我使用的是原生jwt的依赖包,在maven仓库中有好多衍生的jwt依赖包,可自己在maven仓库中选择,实现大同小异. <dependency> <groupI ...

  4. 跟我一起学.NetCore之熟悉的接口权限验证不能少(Jwt)

    前言 权限管控对于一个系统来说是非常重要的,最熟悉不过的是菜单权限和数据权限,上一节通过Jwt实现了认证,接下来用它实现接口权限的验证,为什么不是菜单权限呢?对于前后端分离而言,称其为接口权限感觉比较 ...

  5. SpringSecurity之整合JWT

    SpringSecurity之整合JWT 目录 SpringSecurity之整合JWT 1. 写在前面的话 2. JWT依赖以及工具类的编写 3. JWT过滤器 4. 登录成功结果处理器 5. Sp ...

  6. SpringBoot整合SpringSecurity示例实现前后分离权限注解

    SpringBoot 整合SpringSecurity示例实现前后分离权限注解+JWT登录认证 作者:Sans_ juejin.im/post/5da82f066fb9a04e2a73daec 一.说 ...

  7. (十三)整合 SpringSecurity 框架,实现用户权限管理

    整合 SpringSecurity 框架,实现用户权限管理 1.Security简介 1.1 基础概念 1.2 核心API解读 2.SpringBoot整合SpringSecurity 2.1 流程描 ...

  8. Spring Cloud实战 | 第十一篇:Spring Cloud Gateway 网关实现对RESTful接口权限控制和按钮权限控制

    一. 前言 hi,大家好,这应该是农历年前的关于开源项目 的最后一篇文章了. 有来商城 是基于 Spring Cloud OAuth2 + Spring Cloud Gateway + JWT实现的统 ...

  9. ruoyi接口权限校验

    此文章属于ruoyi项目实战系列 ruoyi系统在前端主要通过权限字符包含与否来动态显示目录和按钮.为了防止通过http请求绕过权限限制,后端接口也需要进行相关权限设计. @PreAuthorize使 ...

随机推荐

  1. vue 父子组件通信详解

    这是一篇详细讲解vue父子组件之间通信的文章,初始学习vue的时候,总是搞不清楚几个情况 通过props在父子组件传值时,v-bind:data="data",props接收的到底 ...

  2. SpringBoot整合Redis(一)

    docker启动redis docker run -p 6379:6379 --name myredis redis 查看容器 [root@topcheer ~]# docker ps -l CONT ...

  3. spring cloud 2.x版本 Ribbon服务发现教程(内含集成Hystrix熔断机制)

    本文采用Spring cloud本文为2.1.8RELEASE,version=Greenwich.SR3 前言 本文基于前两篇文章eureka-server和eureka-client的实现. 参考 ...

  4. 干货:.net core实现读取自定义配置文件,有源代码哦

    看好多人不懂在.NET CORE中如何读取配置文件,我这里分了两篇,上一篇介绍了怎样通过appsettings.json配置读取文件信息.这一篇教大家自定义配置文件: 1.在项目下创建配置文件 { & ...

  5. centos 7 防火墙firewall 与iptables 的一些常用命令

    CentOS 7的防火墙配置跟以前版本有很大区别,CentOS7这个版本的防火墙默认使用的是firewall,与之前的版本使用iptables不一样. firewall常用命令 service fir ...

  6. MIT线性代数:8.求解Ax=b,可解性和结构

  7. 单点登录 - OAuth 2.0 授权码模式(一)

    OAuth 2.0定义了四种授权方式 授权码模式(authorization code) 简化模式(implicit) 密码模式(resource owner password credentials ...

  8. VirtualBox6安装CentOS7设置静态IP

    安装virtualbox后安装centos7, 这里就不在赘述了, 网上有很多教程 先关闭虚拟机, 按照如下设置配置网络 这里需要使用双网卡, 我们在开启第二个网卡, 如下所示 之后开启虚拟机, 进行 ...

  9. Linux修改主机名!(图文)

    本篇作为之前的补充篇,如果想修改自己的主机名,方便老师检查作业是否是自己做的,可以用修改主机名的方法,那么怎么修改呢? 一. 使用hostname命令 比如我现在的主机名是haozhikuan-hbz ...

  10. m113

    今天的比赛很有感触,所以来写一下题解: T1可以发现一些规律是:面积扩大的速度显然比周长扩大的速度快,然后就可以枚举周长来看能为成的面积,其实最优的情况一定是六边型的情况,通过手膜我们可以发现对于边长 ...