在之前的一篇文章中我们已经详细介绍了什么是JWT以及JWT的组成和工作流程。本文将介绍JWT在Spring Boot项目中该如何应用。

一、导入JAR

首先我们使用Maven进行导入,我导入的版本是0.9.0,若需导入其它版本的,请访问mvnrepository获取更多版本的信息

1
2
3
4
5
6
<!-- JSON Web Token Support For The JVM -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

二、编写工具类

该工具类将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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package cn.frankfang.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.List;

/**
* @author: Frank Fang
* @date: 2020/1/31 00:12
* @description: JWT工具类
* JWT是由三段组成的,分别是header(头)、payload(负载)和signature(签名)
* 其中header中放{
* "alg": "HS512",
* "typ": "JWT"
* } 表明使用的加密算法,和token的类型==>默认是JWT
*
*/
public class JwtTokenUtils {

public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";

//密钥,用于signature(签名)部分解密
private static final String PRIMARY_KEY = "MyJwtSecret";
//签发者
private static final String ISS = "FrankFang";
// 添加角色的key
private static final String ROLE_CLAIMS = "role";

// 过期时间是3600秒,即1个小时
private static final long EXPIRATION = 3600L;

// 选择了记住我之后的过期时间为7天
// private static final long EXPIRATION_REMEMBER = 604800L;

/**
* description: 创建Token
*
* @param username
* @param isRememberMe
* @return java.lang.String
*/
public static String createToken(String username, List<String> roles) {
// long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, roles);
return Jwts.builder()
//采用HS512算法对JWT进行的签名,PRIMARY_KEY是我们的密钥
.signWith(SignatureAlgorithm.HS512, PRIMARY_KEY)
//设置角色名
.setClaims(map)
//设置发证人
.setIssuer(ISS)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 24 * 1000))
.compact();
}

/**
* description: 从token中获取用户名
*
* @param token
* @return java.lang.String
*/
public static String getUsername(String token){
return getTokenBody(token).getSubject();
}

// 获取用户角色
@SuppressWarnings("unchecked")
public static List<String> getUserRole(String token){
return (List<String>) getTokenBody(token).get(ROLE_CLAIMS);
}

/**
* description: 判断Token是否过期
*
* @param token
* @return boolean
*/
public static boolean isExpiration(String token){
return getTokenBody(token).getExpiration().before(new Date());
}

/**
* description: 获取
*
* @param token
* @return io.jsonwebtoken.Claims
*/
private static Claims getTokenBody(String token){
return Jwts.parser()
.setSigningKey(PRIMARY_KEY)
.parseClaimsJws(token)
.getBody();
}
}

三、自定义过滤器

由于本项目采用了Spring Security作为安全管理框架,因此需要继承Spring Security的UsernamePasswordAuthenticationFilter类以及BasicAuthenticationFilter类并在Spring Security的配置类中添加这两个自定义的过滤器

1、自定义登录过滤器

编写一个CustomLoginFilter类继承UsernamePasswordAuthenticationFilter

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package cn.frankfang.filter;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.fasterxml.jackson.databind.ObjectMapper;
import cn.frankfang.entity.User;
import cn.frankfang.bean.JsonResponse;
import cn.frankfang.utils.JwtTokenUtils;

public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter {

private AuthenticationManager authenticationManager;

public CustomLoginFilter(AuthenticationManager authenticationManager) {

this.authenticationManager = authenticationManager;
// 设置过滤处理的URL,即登录的
super.setFilterProcessesUrl("/api/login");
}

/**
* 接收并解析用户凭证,出現错误时,返回JSON数据前端
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
try {
User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
} catch (Exception e) {
e.printStackTrace();
try {
// 未登录出现账号或密码错误,使用JSON进行提示
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
JsonResponse result = new JsonResponse(HttpServletResponse.SC_UNAUTHORIZED, "账号或密码错误!");
out.write(new ObjectMapper().writeValueAsString(result));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
throw new RuntimeException();
}

}

/**
* 用户登录成功后,生成token,并且返回JSON数据给前端
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication auth) {

try {
// 获取用户
User user = (User) auth.getPrincipal();
// 获取角色信息
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
List<String> roles = new ArrayList<>();
for (GrantedAuthority authority : authorities) {
roles.add(authority.getAuthority());
}

// JSON Web Token构建
String token = JwtTokenUtils.createToken(user.getUsername(), roles);

try {
// 登录成功时,返回JSON格式进行提示
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
Map<String, Object> data = new HashMap<>();
data.put("id", user.getId());
data.put("username", user.getUsername());
data.put("token", "Bearer " + token);
JsonResponse result = new JsonResponse(HttpServletResponse.SC_OK, "登录成功!", data);
out.write(new ObjectMapper().writeValueAsString(result));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

用户实体类User部分内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class User implements UserDetails, Serializable {
// 用户id
private Integer id;
// 用户名
private String username;
// 账户密码
private String password;
// 用户角色列表
private List<Role> roles;

/* 以下内容省略 */
}

用户角色类Role部分内容如下:

1
2
3
4
5
6
7
8
public class Role implements Serializable {
// 角色id
private Integer id;
// 角色名
private String name;

/* 以下内容省略 */
}

自定义JSON格式类JsonResponse部分内容如下:

1
2
3
4
5
6
7
8
9
10
public class JsonResponse {
// 状态码
private Integer status;
// 消息
private String message;
// 数据
private Object data;

/* 以下内容省略 */
}

2、自定义授权过滤器

编写CustomAuthenticationFilter类继承BasicAuthenticationFilter

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package cn.frankfang.filter;

import java.io.PrintWriter;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import com.fasterxml.jackson.databind.ObjectMapper;
import cn.frankfang.utils.JwtTokenUtils;

public class CustomAuthenticationFilter extends BasicAuthenticationFilter {

public CustomAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
try {
//请求体的头中是否包含Authorization
String header = request.getHeader("Authorization");
//Authorization中是否包含Bearer,有一个不包含时直接返回
if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
// responseJson(response);
return;
}
//获取权限失败,会抛出异常
UsernamePasswordAuthenticationToken authentication = getAuthentication(header);
//获取后,将Authentication写入SecurityContextHolder中供后序使用
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} catch (Exception e) {
responseJson(response);
e.printStackTrace();
}
}

private void responseJson(HttpServletResponse response) {
try {
//未登录时使用JSON进行提示
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
Map<String,Object> map = new HashMap<>();
map.put("status",HttpServletResponse.SC_FORBIDDEN);
map.put("message", "请登录");
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
} catch (Exception e1) {
e1.printStackTrace();
}
}

private UsernamePasswordAuthenticationToken getAuthentication(String header) {
// 将头部固定的TOKEN_PREFIX去除
String token = header.replace(JwtTokenUtils.TOKEN_PREFIX, "");
// 获取用户名
String username = JwtTokenUtils.getUsername(token);
// 获取用户角色列表
List<String> roles = JwtTokenUtils.getUserRole(token);
Collection<GrantedAuthority> authorities = new HashSet<>();
if (roles!=null) {
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
}
if (username != null){
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
return null;
}
}

四、添加过滤器

在Spring Security的配置类中添加过滤器

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
package cn.frankfang.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import cn.frankfang.filter.CustomAuthenticationFilter;
import cn.frankfang.filter.CustomLoginFilter;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService service;

@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//从数据库中调取用户信息(UserName, Password, Role)
auth.userDetailsService(service);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
// 定制请求的授权规则
http.authorizeRequests()
.antMatchers("/api/admin/**").hasRole("admin")
.anyRequest().permitAll();

// 允许跨域
http.cors();

// 由于使用的是JWT,我们这里不需要csrf
http.csrf().disable();

// 基于token,所以不需要session
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests();

// 添加登录过滤器
http.addFilter(new CustomLoginFilter(authenticationManager()));

// 添加授权过滤器
http.addFilter(new CustomAuthenticationFilter(authenticationManager()));
}
}

五、测试

在整合完成之后,我们便可以启动项目进行测试,这里我们使用Postman模拟前端进行登录:

登录成功

登录成功后JWT生成的Token:

1
Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBRE1JTiIsInJvbGUiOlsiUk9MRV9hZG1pbiJdLCJpc3MiOiJGcmFua0ZhbmciLCJleHAiOjE2MDc3NDgzMTEsImlhdCI6MTYwNzY2MTkxMX0.GS2KZ5LIJeCsLL88idGHSo68THzRv6TwMa0knNITqSquV-iY8-wrnyezIKNy7zKAsuy9NFVzSpUWOuqOZwxL9A

之后在发起需要进行权限验证的请求时需要在请求头中添加Authorization请求头,值为JWT生成的Token

访问受保护的资源

当可以正常获取服务端响应的数据时便代表整合成功。