Spring Boot+Spring Security+JWT 实现 RESTful Api 权限控制

Stella981
• 阅读 823

摘要:用spring-boot开发RESTful API非常的方便,在生产环境中,对发布的API增加授权保护是非常必要的。现在我们来看如何利用JWT技术为API增加授权保护,保证只有获得授权的用户才能够访问API。

一:开发一个简单的API

在IDEA开发工具中新建一个maven工程,添加对应的依赖如下:

  1. org.springframework.boot

  2. spring-boot-starter

  3. org.springframework.boot

  4. spring-boot-starter-test

  5. test

  6. org.springframework.boot

  7. spring-boot-starter-web

  8. org.springframework.boot

  9. spring-boot-starter-data-jpa

  10. mysql

  11. mysql-connector-java

  12. 5.1.30

  13. org.springframework.boot

  14. spring-boot-starter-security

  15. io.jsonwebtoken

  16. jjwt

  17. 0.7.0

新建一个UserController.java文件,在里面在中增加一个hello方法:

  1. @RequestMapping("/hello")

  2. @ResponseBody

  3. public String hello(){

  4. return "hello";

  5. }

这样一个简单的RESTful API就开发好了。

现在我们运行一下程序看看效果,执行JwtauthApplication.java类中的main方法:

等待程序启动完成后,可以简单的通过curl工具进行API的调用,如下图:

Spring Boot+Spring Security+JWT 实现 RESTful Api 权限控制

至此,我们的接口就开发完成了。但是这个接口没有任何授权防护,任何人都可以访问,这样是不安全的,下面我们开始加入授权机制。

二:增加用户注册功能

首先增加一个实体类User.java:

  1. package boss.portal.entity;

  2. import javax.persistence.*;

  3. /**

  4. * @author zhaoxinguo on 2017/9/13.

  5. */

  6. @Entity

  7. @Table(name = "tb_user")

  8. public class User {

  9. @Id

  10. @GeneratedValue

  11. private long id;

  12. private String username;

  13. private String password;

  14. public long getId() {

  15. return id;

  16. }

  17. public String getUsername() {

  18. return username;

  19. }

  20. public void setUsername(String username) {

  21. this.username = username;

  22. }

  23. public String getPassword() {

  24. return password;

  25. }

  26. public void setPassword(String password) {

  27. this.password = password;

  28. }

  29. }

然后增加一个Repository类UserRepository,可以读取和保存用户信息:

  1. package boss.portal.repository;

  2. import boss.portal.entity.User;

  3. import org.springframework.data.jpa.repository.JpaRepository;

  4. /**

  5. * @author zhaoxinguo on 2017/9/13.

  6. */

  7. public interface UserRepository extends JpaRepository<User, Long> {

  8. User findByUsername(String username);

  9. }

在UserController类中增加注册方法,实现用户注册的接口:

  1. /**

  2. * 该方法是注册用户的方法,默认放开访问控制

  3. * @param user

  4. */

  5. @PostMapping("/signup")

  6. public void signUp(@RequestBody User user) {

  7. user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));

  8. applicationUserRepository.save(user);

  9. }

其中的 @PostMapping("/signup")

这个方法定义了用户注册接口,并且指定了url地址是/users/signup。由于类上加了注解 @RequestMapping(“/users”),类中的所有方法的url地址都会有/users前缀,所以在方法上只需指定/signup子路径即可。

密码采用了BCryptPasswordEncoder进行加密,我们在Application中增加BCryptPasswordEncoder实例的定义。

  1. package boss.portal;

  2. import org.springframework.boot.SpringApplication;

  3. import org.springframework.boot.autoconfigure.SpringBootApplication;

  4. import org.springframework.context.annotation.Bean;

  5. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

  6. @SpringBootApplication

  7. public class JwtauthApplication {

  8. @Bean

  9. public BCryptPasswordEncoder bCryptPasswordEncoder() {

  10. return new BCryptPasswordEncoder();

  11. }

  12. public static void main(String[] args) {

  13. SpringApplication.run(JwtauthApplication.class, args);

  14. }

  15. }

三:增加JWT认证功能

用户填入用户名密码后,与数据库里存储的用户信息进行比对,如果通过,则认证成功。传统的方法是在认证通过后,创建sesstion,并给客户端返回cookie。现在我们采用JWT来处理用户名密码的认证。区别在于,认证通过后,服务器生成一个token,将token返回给客户端,客户端以后的所有请求都需要在http头中指定该token。服务器接收的请求后,会对token的合法性进行验证。验证的内容包括:

  1. 内容是一个正确的JWT格式

  2. 检查签名

  3. 检查claims

  4. 检查权限

处理登录

创建一个类JWTLoginFilter,核心功能是在验证用户名密码正确后,生成一个token,并将token返回给客户端:

  1. package boss.portal.web.filter;

  2. import boss.portal.entity.User;

  3. import com.fasterxml.jackson.databind.ObjectMapper;

  4. import io.jsonwebtoken.Jwts;

  5. import io.jsonwebtoken.SignatureAlgorithm;

  6. import org.springframework.security.authentication.AuthenticationManager;

  7. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

  8. import org.springframework.security.core.Authentication;

  9. import org.springframework.security.core.AuthenticationException;

  10. import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

  11. import javax.servlet.FilterChain;

  12. import javax.servlet.ServletException;

  13. import javax.servlet.http.HttpServletRequest;

  14. import javax.servlet.http.HttpServletResponse;

  15. import java.io.IOException;

  16. import java.util.ArrayList;

  17. import java.util.Date;

  18. /**

  19. * 验证用户名密码正确后,生成一个token,并将token返回给客户端

  20. * 该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法

  21. * attemptAuthentication :接收并解析用户凭证。

  22. * successfulAuthentication :用户成功登录后,这个方法会被调用,我们在这个方法里生成token。

  23. * @author zhaoxinguo on 2017/9/12.

  24. */

  25. public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {

  26. private AuthenticationManager authenticationManager;

  27. public JWTLoginFilter(AuthenticationManager authenticationManager) {

  28. this.authenticationManager = authenticationManager;

  29. }

  30. // 接收并解析用户凭证

  31. @Override

  32. public Authentication attemptAuthentication(HttpServletRequest req,

  33. HttpServletResponse res) throws AuthenticationException {

  34. try {

  35. User user = new ObjectMapper()

  36. .readValue(req.getInputStream(), User.class);

  37. return authenticationManager.authenticate(

  38. new UsernamePasswordAuthenticationToken(

  39. user.getUsername(),

  40. user.getPassword(),

  41. new ArrayList<>())

  42. );

  43. } catch (IOException e) {

  44. throw new RuntimeException(e);

  45. }

  46. }

  47. // 用户成功登录后,这个方法会被调用,我们在这个方法里生成token

  48. @Override

  49. protected void successfulAuthentication(HttpServletRequest req,

  50. HttpServletResponse res,

  51. FilterChain chain,

  52. Authentication auth) throws IOException, ServletException {

  53. String token = Jwts.builder()

  54. .setSubject(((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername())

  55. .setExpiration( new Date(System.currentTimeMillis() + 60 * 60 * 24 * 1000))

  56. .signWith(SignatureAlgorithm.HS512, "MyJwtSecret")

  57. .compact();

  58. res.addHeader( "Authorization", "Bearer " + token);

  59. }

  60. }

该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法:

attemptAuthentication :接收并解析用户凭证。

successfulAuthentication :用户成功登录后,这个方法会被调用,我们在这个方法里生成token。

授权验证

用户一旦登录成功后,会拿到token,后续的请求都会带着这个token,服务端会验证token的合法性。

创建JWTAuthenticationFilter类,我们在这个类中实现token的校验功能。

  1. package boss.portal.web.filter;

  2. import io.jsonwebtoken.Jwts;

  3. import org.springframework.security.authentication.AuthenticationManager;

  4. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

  5. import org.springframework.security.core.context.SecurityContextHolder;

  6. import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

  7. import javax.servlet.FilterChain;

  8. import javax.servlet.ServletException;

  9. import javax.servlet.http.HttpServletRequest;

  10. import javax.servlet.http.HttpServletResponse;

  11. import java.io.IOException;

  12. import java.util.ArrayList;

  13. /**

  14. * token的校验

  15. * 该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,

  16. * 从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。

  17. * 如果校验通过,就认为这是一个取得授权的合法请求

  18. * @author zhaoxinguo on 2017/9/13.

  19. */

  20. public class JWTAuthenticationFilter extends BasicAuthenticationFilter {

  21. public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {

  22. super(authenticationManager);

  23. }

  24. @Override

  25. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

  26. String header = request.getHeader( "Authorization");

  27. if (header == null || !header.startsWith("Bearer ")) {

  28. chain.doFilter(request, response);

  29. return;

  30. }

  31. UsernamePasswordAuthenticationToken authentication = getAuthentication(request);

  32. SecurityContextHolder.getContext().setAuthentication(authentication);

  33. chain.doFilter(request, response);

  34. }

  35. private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {

  36. String token = request.getHeader( "Authorization");

  37. if (token != null) {

  38. // parse the token.

  39. String user = Jwts.parser()

  40. .setSigningKey( "MyJwtSecret")

  41. .parseClaimsJws(token.replace( "Bearer ", ""))

  42. .getBody()

  43. .getSubject();

  44. if (user != null) {

  45. return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());

  46. }

  47. return null;

  48. }

  49. return null;

  50. }

  51. }

该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。如果校验通过,就认为这是一个取得授权的合法请求。

SpringSecurity配置

通过SpringSecurity的配置,将上面的方法组合在一起。

  1. package boss.portal.security;

  2. import boss.portal.web.filter.JWTLoginFilter;

  3. import boss.portal.web.filter.JWTAuthenticationFilter;

  4. import org.springframework.boot.autoconfigure.security.SecurityProperties;

  5. import org.springframework.context.annotation.Configuration;

  6. import org.springframework.core.annotation.Order;

  7. import org.springframework.http.HttpMethod;

  8. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

  9. import org.springframework.security.config.annotation.web.builders.HttpSecurity;

  10. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

  11. import org.springframework.security.core.userdetails.UserDetailsService;

  12. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

  13. /**

  14. * SpringSecurity的配置

  15. * 通过SpringSecurity的配置,将JWTLoginFilter,JWTAuthenticationFilter组合在一起

  16. * @author zhaoxinguo on 2017/9/13.

  17. */

  18. @Configuration

  19. @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)

  20. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  21. private UserDetailsService userDetailsService;

  22. private BCryptPasswordEncoder bCryptPasswordEncoder;

  23. public WebSecurityConfig(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {

  24. this.userDetailsService = userDetailsService;

  25. this.bCryptPasswordEncoder = bCryptPasswordEncoder;

  26. }

  27. @Override

  28. protected void configure(HttpSecurity http) throws Exception {

  29. http.cors().and().csrf().disable().authorizeRequests()

  30. .antMatchers(HttpMethod.POST, "/users/signup").permitAll()

  31. .anyRequest().authenticated()

  32. .and()

  33. .addFilter( new JWTLoginFilter(authenticationManager()))

  34. .addFilter( new JWTAuthenticationFilter(authenticationManager()));

  35. }

  36. @Override

  37. public void configure(AuthenticationManagerBuilder auth) throws Exception {

  38. auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);

  39. }

  40. }

这是标准的SpringSecurity配置内容,就不在详细说明。注意其中的

.addFilter(new JWTLoginFilter(authenticationManager()))  .addFilter(new JwtAuthenticationFilter(authenticationManager()))

这两行,将我们定义的JWT方法加入SpringSecurity的处理流程中。

下面对我们的程序进行简单的验证:

# 请求hello接口,会收到403错误,如下图:

curl http://localhost:8080/hello

Spring Boot+Spring Security+JWT 实现 RESTful Api 权限控制

# 注册一个新用户curl -H"Content-Type: application/json" -X POST -d '{"username":"admin","password":"password"}' http://localhost:8080/users/signup

如下图:

Spring Boot+Spring Security+JWT 实现 RESTful Api 权限控制

# 登录,会返回token,在http header中,Authorization: Bearer 后面的部分就是tokencurl -i -H"Content-Type: application/json" -X POST -d '{"username":"admin","password":"password"}' http://localhost:8080/login

如下图:

Spring Boot+Spring Security+JWT 实现 RESTful Api 权限控制

# 用登录成功后拿到的token再次请求hello接口# 将请求中的XXXXXX替换成拿到的token# 这次可以成功调用接口了curl -H"Content-Type: application/json" \-H"Authorization: Bearer XXXXXX" \"http://localhost:8080/users/hello"

如下图:

Spring Boot+Spring Security+JWT 实现 RESTful Api 权限控制

五:总结

至此,给SpringBoot的接口加上JWT认证的功能就实现了,过程并不复杂,主要是开发两个SpringSecurity的filter,来生成和校验JWT token。

JWT作为一个无状态的授权校验技术,非常适合于分布式系统架构,因为服务端不需要保存用户状态,因此就无需采用redis等技术,在各个服务节点之间共享session数据。

六:源码下载地址:

地址: https://gitee.com/micai/springboot-springsecurity-jwt-demo

七:建议及改进:
若您有任何建议,可以通过1)加入qq群715224124向群主提出,或2)发送邮件至827358369@qq.com向我反馈。本人承诺,任何建议都将会被认真考虑,优秀的建议将会被采用,但不保证一定会在当前版本中实现。

八:鸣谢地址:

http://blog.csdn.net/haiyan_qi/article/details/77373900

https://segmentfault.com/a/1190000009231329

http://www.jianshu.com/p/6307c89fe3fa

http://www.cnblogs.com/grissom007/p/6294746.html

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这