为了提高网站的api的安全性,一般都会实现认证(Autentication)和授权(Authenrization),分别对应用户的登录、控制用户可以访问的资源。

JWT介绍

什么是JWT(Json Web Token)

详情请见jwt官网.直观来讲,它是一个格式为形如aaaa.bbbb.cccc的字符串token,它存放了登录用户的信息,也是用户登录网站的凭证。

为什么要用JWT?

当我们登录了一个网站,然后因为手头有急事把浏览器关闭转而做其他事,但你不用担心下次重新打开浏览器后网站已经失去了登录状态(前提是你间隔的时间没有大于jwt的过期时间),这很方便。当然你会说,Session也可以实现这个功能,But在使用Session的同时,会增加服务器的存储压力,而JWT是将存储的压力分布到各个客户端,减轻服务器的压力。

JWT长什么样

JWT由三个子字符串组成

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

写成一行,就是这个样子:Header.Payload.Signature

  1. Header

Header是由一下这个格式的Json通过Base64编码生成的字符串(注意:Base64编码不是加密,加密一般不可破解,编码可以通过用编码反编码或得原来的Json数据,因此不安全,所以不存放敏感信息),Header中存放的内容是说明编码对象是一个JWT以及使用“SHA-256”的算法进行加密(加密用于生成Signature签名)

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}
  1. Payload

虽然第二部分官网上写的是Payload,但你要认识到Payload是base64编码得到的字符串,编码之前它是个Json格式的数据,我们把它称作Claim,也就是Claim---Base64编码-->Payload.这也很好理解,那Claim里存放的是什么数据呢,它存放JWT自身的标准属性,所有的标准属性都是可选的,比如有:

  • “iss”:”Issuer —— 签发人”,
  • “sub”:”Subject —— 主题”,
  • “aud”:”Audience —— 受众”,
  • “exp”:”Expiration Time —— 过期时间”,
  • “nbf”:”Not Before —— 生效时间”,
  • “iat”:”Issued At —— 签发时间”,
  • “jti”:”JWT ID —— 编号”

除了以上的JWT官方标准属性,你还可以在这个部分添加自定义字段,以下的例子中admin即自定义字段。

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
  1. Signature

Signature 是由Header和Payload组合而成,将Header和Claim这两个Json分别使用Base64方式进行编码,生成字符串Header和Payload,然后将Header和Payload以Header.Payload的格式组合在一起形成一个字符串,然后使用Header里指定的签名算法和一个密匙(这个secret存放在服务器上,用于进行验证)对这个字符串进行加密,形成一个新的字符串,公式如下

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

SpringBoot使用JWT

SpringBoot整合JWT的方式有很多,也有第三方的库,比如auth0,但是我们不使用。我们最终需要实现两个功能,一个是实现用户登录(Authentication),另一个是用户授权(Authenrization)。工程目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.
└── src
├── main
│   ├── java
│   │   └── com
│   │   └── bababadboy
│   │   ├── dealermng
│   │   │   ├── Application.java
│   │   │   ├── config
│   │   │   │   ├── SecurityConfig.java
│   │   │   │   └── WebMvcConfigurer.java
│   │   │   ├── controller
│   │   │   │   ├── ProductController.java
│   │   │   │   └── UserController.java
│   │   │   ├── dto
│   │   │   │   └── UserDataDTO.java
│   │   │   ├── entity
│   │   │   │   ├── Product.java
│   │   │   │   └── user
│   │   │   │   ├── Role.java
│   │   │   │   └── User.java
│   │   │   ├── repository
│   │   │   │   ├── ProductRepository.java
│   │   │   │   └── UserRepository.java
│   │   │   ├── security
│   │   │   │   ├── JwtCfg.java
│   │   │   │   ├── JwtFilter.java
│   │   │   │   ├── JwtTokenFilterConfigurer.java
│   │   │   │   ├── JwtTokenProvider.java
│   │   │   └── service
│   │   │   ├── ProductService.java
│   │   │   └── impl
│   │   │   ├── ProductServiceImpl.java
│   │   │   └── UserService.java
│   │   └── springboot1
│   └── resources
——...
  • JwtCfg类
    这个类中声明了一个@Bean ,用于生成一个过滤器类,对/api 链接下的所有资源访问进行JWT的验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author wangxiaobin
*/
@Configuration
public class JwtCfg {
@Bean
public FilterRegistrationBean<JwtFilter> jwtFilter() {
FilterRegistrationBean<JwtFilter> registrationBean;
registrationBean = new FilterRegistrationBean<JwtFilter>();
registrationBean.setFilter(new JwtFilter());
registrationBean.addUrlPatterns("/api/*");

return registrationBean;
}
}
  • JwtFilter类
    这个类声明了一个JWT过滤器类,从Http请求中提取JWT的信息,并使用了”secretkey”这个密匙对JWT进行验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 从Http请求中提取JWT的信息,并使用了”secretkey”这个密匙对JWT进行验证
* @author wangxiaobin
*/
public class JwtFilter extends GenericFilterBean {
public void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain)
throws IOException, ServletException {

// Change the req and res to HttpServletRequest and HttpServletResponse
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;

// Get authorization from Http request
final String authHeader = request.getHeader("authorization");

// If the Http request is OPTIONS then just return the status code 200
// which is HttpServletResponse.SC_OK in this code
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);

chain.doFilter(req, res);
}
// Except OPTIONS, other request should be checked by JWT
else {

// Check the authorization, check if the token is started by "Bearer "
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new ServletException("Missing or invalid Authorization header");
}

// Then get the JWT token from authorization
final String token = authHeader.substring(7);

try {
// Use JWT parser to check if the signature is valid with the Key "secretkey"
// 使用了”secretkey”这个密匙对JWT进行验证
final Claims claims = Jwts.parser().setSigningKey("secretkey").parseClaimsJws(token).getBody();

// Add the claim to request header
request.setAttribute("claims", claims);
} catch (final SignatureException e) {
throw new ServletException("Invalid token");
}

chain.doFilter(req, res);
}
}
}

UserController类,用户的注册和登录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @author wangxiaobin
*/
@RestController
@RequestMapping("/users")
public class UserController {

private final UserService userService;
private final ModelMapper modelMapper;

@Autowired
public UserController(UserService userService, ModelMapper modelMapper) {
this.userService = userService;
this.modelMapper = modelMapper;
}

@PostMapping("/signup")
public String signUp(@RequestBody User user) {
return userService.signUp(user);
}

@PostMapping("/login")
public Object logIn(@RequestParam String username,@RequestParam String password){
String token = userService.logIn(username,password);
UserDataDTO info = modelMapper.map(userService.search(username),UserDataDTO.class);
JSONObject result = new JSONObject();
result.put("userMsg",info);
result.put("accessToken",token);
return JSON.toJSON(result);
}
}
  • jwt工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* @author wangxiaobin
*/
@Component
public class JwtTokenProvider {
/**
* THIS IS NOT A SECURE PRACTICE! For simplicity, we are storing a static key here. Ideally, in a
* microservices environment, this key would be kept on a config-server.
*/
@Value("${security.jrwt.token.secret-key:secret-key}")
private String secretKey;

@Value("${security.jwt.token.expire-length:3600000}")
private long validityInMilliseconds = 3600000; // 1h

@Autowired
private MyUserDetails myUserDetails;

@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}

/**
* 生成jwt
*/
public String createToken(String username, List<Role> roles) {

Claims claims = Jwts.claims().setSubject(username);
claims.put("auth", roles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority())).filter(Objects::nonNull).collect(Collectors.toList()));

Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);

return Jwts.builder()//
.setClaims(claims)//
.setIssuedAt(now)//
.setExpiration(validity)//
.signWith(SignatureAlgorithm.HS256, "secretkey")//
.compact();
}

public Authentication getAuthentication(String token) {
UserDetails userDetails = myUserDetails.loadUserByUsername(getUsername(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}

public String getUsername(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}


/**
* 分解request,获取token
*/
public String resolveToken(HttpServletRequest req) {
String bearerToken = req.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}

public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
throw new CustomException("Expired or invalid JWT token", HttpStatus.INTERNAL_SERVER_ERROR);
}
}

}
  • ProductController类,测试JWT功能,只有当用户认证成功之后,/api下的资源才能被访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 描述:
* Product控制器
*
* @author wangxiaobin
*/
@Transactional
@RestController
@RequestMapping(value = "/api")
public class ProductController{
private final ProductRepository productRepository;
private final ProductServiceImpl productService;

@Autowired
public ProductController(ProductRepository productRepository, ProductServiceImpl productService) {
this.productRepository = productRepository;
this.productService = productService;
}
/**
* 产品列表分页查询
*/
@RequestMapping(value = "/products",method = RequestMethod.GET)
public Object retrieveAllProducts(@RequestParam(value = "page", defaultValue = "0") Integer page ,
@RequestParam(value = "size", defaultValue = "15") Integer size){
return JSON.toJSON(productService.findProductNoCriteria(page,size).getContent());
}

/**
* 根据产品编号"no"查询商品详情
*/
@GetMapping(value = "/products/{no}")
public Object retrieveProduct(@PathVariable("no") String no) {
Optional<Product> product = productService.retrieveProduct(no);
return JSON.toJSON(product);
}
}

代码功能测试

  1. 首先直接访问/api下的product资源,毫无疑问是不能访问的。
    500
  2. 然后进行一个新的测试用户的注册,可以看到注册成功的提示返回

signup

  1. 再让该用户进行登录,可以看到登录成功之后返回的JWT字符串

login

  1. 最后用获取到的JWT作为访问令牌,申请访问/api/products,可以访问成功。

get

参考链接:
https://jwt.io/
http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html
https://blog.csdn.net/ltl112358/article/details/79507148

Comments

⬆︎TOP