Security - JWT介绍
笔记
JWT 配合 Spring Security 使用将会使你的项目更加安全,这也是主流。
2021-12-25 @Young Kbt
# 认证授权过程
(1)如果是基于 Session,那么 Spring Security 会对 cookie 里的 sessionid 进行解析,找到服务器存储的 session 信息,然后判断当前用户是否符合请求的要求。
(2)如果是 token,则是解析出 token,然后将当前请求加入到 Spring Security 管理的权限信息中
如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于 token 的形式 进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值,根据用户名相关信息生成 token 返回给浏览器,浏览器将 token 记录到 cookie 中,每次调用 api 接口都默认将 token 携带到 header 请求头中,Spring Security 解析 header 头获取 token 信息,解析 token 获取当前用户名,根据用户名就可以从 mysql 或者 redis 中获取权限列表,这样 Spring Security 就能够判断当前请求是否有权限访问。
# JWT组成
JWT 的核心 token 俗称令牌。
# 令牌类型
先看一个 token 长什么样
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJrZWxlIiwiZXhwIjoxNjQwNDA0MzgzLCJjcmVhdGVkIjoxNjQwMzYxMTgzMjI0LCJhdXRob3JpdGllcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9OT1JNQUwifSx7ImF1dGhvcml0eSI6IlJPTEVfQURNSU4ifV19.BL_XFikpxXxzzH6hI_3lJgB7IWezyM4m33IISV6O5USeq5z1xuWf8rB02S-Rg8tdmzNBFVHXvZO-zgdFYE8mIQ
可以看到 token 是一个很长的字符串,字符之间通过「 . 」分隔符分为三个子串,每一个子串表示了一个功能块,总共有以下三个部分:JWT 头、有效载荷和签名。
# JWT头
JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示。
{
"alg": "HS256",
"typ": "JWT"
}
2
3
4
在上面的代码中,alg 属性表示签名使用的算法,默认为 HMAC SHA256(写为 HS256);typ 属性表示令牌的类型,JWT 令牌统一写为 JWT。最后,使用 Base64 URL 算法将上述 JSON 对象转换为字符串保存。
# 有效载荷
有效载荷部分,是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据。
JWT 指定七个默认字段供选择:
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID 用于标识该 JWT 除以上默认字段外,我们还可以自定义私有字段,如下:
{ "sub": "1234567890", "name": "Helen", "admin": true }
1
2
3
4请注意,默认情况下 JWT 是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。
JSON 对象也使用 Base64 URL 算法转换为字符串保存。
# 签名哈希
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为 HMAC SHA256)根据以下公式生成签名:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)
在计算出签名哈希后,JWT 头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用「 . 」分隔,就构成整个 JWT 对象。
# Base64URL算法
如前所述,JWT 头和有效载荷序列化的算法都用到了 Base64URL。该算法和常见 Base64 算法类似,稍有差别。
作为令牌的 JWT 可以放在 URL 中(例如 api.example/?token=xxx
)。 Base64 中用的三个字符是 "+","/" 和 "=",由于在 URL 中有特殊含义,因此 Base64URL 中对他们做了替换:"=" 去掉,"+" 用 "-" 替换,"/" 用 "_" 替换,这就是 Base64URL 算法。
# 实战
该实战不推荐看了,比较粗糙,几天后我已经写了新的实战,请看 Security - JWT登录实战,那个内容的一些工具类绝对是生产开发需要的,可以将其收藏。
并不是说本次实战不要看,本内容也有一些可取之处,如业务类和新实战的业务类不太一样,但是两者都是常用的,具体使用哪个,看你的需求。
目录结构如下:
包名 cn.kbt
├── bean (数据库对应的实体类包)
│ ├── User
│ ├── Role
│ ├── SecurityUser
├── config (Security 配置类)
│ ├── TokenWebSecurityConfig
├── controller
│ ├── JwtAuthController
├── mapper
│ ├── UserMapper
├── security (Security 实现类)
│ ├── TokenManager
│ ├── TokenLogoutHandler
│ ├── UnauthorizedEntryPoint
├── service
│ ├── impl
│ └── UserServiceImpl
│ ├── UserService
│
└── SpringSecurityApplication
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 实体类
和数据库对应的实体类 User,记得加上 set 和 get 方法
public class User {
private String username;
private String password;
private List<Role> roleList;
// set 和 get 方法
}
2
3
4
5
6
7
和数据库对应的权限类 Role,记得加上 set 和 get 方法
public class Role {
private Long id;
private String roleName;
// set 和 get 方法
}
2
3
4
5
6
和 Spring Security 对应的 SecurityUser 类,该类存储用户信息,然后放到 Spring Security 里
public class SecurityUser implements UserDetails {
// 当前登录用户
private transient User currentUserInfo;
// 当前权限列表
private List<String> permissionValueList;
public SecurityUser() {
}
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) {
continue;
}
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
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
# 认证授权工具类
token 操作的工具类:TokenManager
,token 由 JWT 头(JWT 生成等)、有效负载(用户信息等)、签名(自定义字符串等)组成
@Component
public class TokenManager {
// 指定 token 生命时长
private long tokenLife = 24 * 60 * 60 * 1000;
// 指定编码和解码的字符串,即签名
private String tokenSignKey = "123456";
public String createToken(String username) {
String token = Jwts.builder().setSubject(username) // token 的主体部分:用户信息
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) // token的有效时长:一天
.signWith(SignatureAlgorithm.HS512, tokenSignKey) // token 的签名
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
public String getUserFromToken(String token) {
String user = Jwts.parser()
.setSigningKey(tokenSignKey)
.parseClaimsJws(token)
.getBody()
.getSubject();
return user;
}
public void removeToken(String token) {
// jwt 的 token 无需删除,客户端扔掉即可。
}
}
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
用户登出的工具类:TokenLogoutHandler
,退出登录后删除 Token,并且在 Redis 数据库删除相关信息
@Component
public class TokenLogoutHandler implements LogoutHandler {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
String token = request.getHeader("token");
if (token != null) {
tokenManager.removeToken(token);
// 清空当前用户缓存中的权限数据
String userName = tokenManager.getUserFromToken(token);
redisTemplate.delete(userName);
}
ResponseUtil.out(response, R.ok());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
未授权统一处理:UnauthorizedEntryPoint
@Component
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
2
3
4
5
6
7
8
9
# 认证授权拦截器
认证的拦截器:TokenLoginFilter
。用户登录成功,生成 Token,存入数据库,登录失败返回失败结果
@Component
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLoginFilter(AuthenticationManager authenticationManager,
TokenManager tokenManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
}
/**
* 用户登录时,处理请求
* 1. 获取用户操作的表单信息
* 2. 进行存入权限封装的类里
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 获取权限封装的类
* 登录成功则生成 token,并把相关信息存入数据库
* 相关信息指的是,用户名,密码,权限信息
*/
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException, ServletException {
SecurityUser user = (SecurityUser) auth.getPrincipal();
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
ResponseUtil.out(res, R.ok().data("token", token));
}
/**
* 登录失败
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
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
授权拦截器:TokenAuthenticationFilter
。登录成功后,进行权限认证(认证成功将用户信息存入上下文),源码已经实现,不需要自己手动配置
@Component
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenAuthenticationFilter(AuthenticationManager authManager,
TokenManager tokenManager,
RedisTemplate redisTemplate) {
super(authManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
/**
* 将权限存入上下文中
*/
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
if(req.getRequestURI().indexOf("admin") == -1) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = authentication = getAuthentication(req);
if (authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
ResponseUtil.out(res, R.error());
}
// 放行资源
chain.doFilter(req, res);
}
/**
* 通过请求获取请求头的 token
*/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// token 置于 header 里
String token = request.getHeader("token");
if (token != null && !"".equals(token.trim())) {
String userName = tokenManager.getUserFromToken(token);
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
if (!StringUtils.isEmpty(userName)) {
return new UsernamePasswordAuthenticationToken(userName, token, authorities);
}
return null;
}
return null;
}
}
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
# 业务类
其他的类我就不写了,就是 service 层、mapper 层,至于数据库的三个张表(用户表、角色表、用户角色关联表),效果如图:
提供 findUserByUsername
方法的 sql:
select r.* from sys_user u,sys_role r,user_role ur where u.id = ur.user_id and r.id = ur.role_id and username = #{username}
最重要的是这个业务类,自定义类继承 UserDetailsService,实现 Spring Security 的业务,重写方法。该类将由 Spring Security 自动调用。
认证授权拦截器 TokenLoginFilter
的 26 行代码 authenticationManager.authenticate
的内部就会触发下面实现的 loadUserByUsername
方法,进行数据库的交互,用户名和密码判断是否正确
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("该用户不存在");
}
// 创建 GrantedAuthority 的 List 集合,用于存储 GrantedAuthority
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
// 创建单个 GrantedAuthority,用户存储用户信息
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(userInfo.getRoleList());
// 存储进去
grantedAuthorities.add(grantedAuthority);
return new JwtUserDetails(user.getUsername(), user.getPassword(), grantedAuthorities);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 配置类
前面内容写完,不放到核心配置类里,等于白写,核心配置类内容如下:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private TokenManager tokenManager;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private BCryptPasswordEncoder bcryptPasswordEncoder;
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
/**
* 核心配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new UnAuthEntryPoint()) // 没有权限访问的处理
.and().authorizeRequests() // 开启认证
.anyRequest().authenticated()// 任何请求需要认证
.and().logout().logoutUrl("/logout") // 退出操作
.addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)) // 退出后token的操作
// 权限认证处理类
.and().addFilter(new TokenAuthFilter(authenticationManager(),tokenManager,redisTemplate))
// 添加自定义的过滤器,即用户登录后 token 的创建
.addFilter(new TokenLoginFilter(authenticationManager(),tokenManager,redisTemplate)).httpBasic()
.and().csrf().disable();
}
/**
* 调用 userDetailsService 和密码处理
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bcryptPasswordEncoder);
}
/**
* 不进行认证的路径设置,可以直接访问
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**", "/other/**");
}
}
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
# 测试类
编写一个 Controller,用于登录
@RestController
public class JwtAuthController {
@Autowired
private UserService userService;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
private TokenManager tokenManager;
@PostMapping("/login")
public String login(@RequestParam("username") String username, @RequestParam("password") String password, HttpServletRequest request) {
User user = userService.findByUsername(username);
// 账号不存在
if (user == null) {
return HttpResult.error("账号不存在");
// 数据库的密码是加密过的
}else if(!bCryptPasswordEncoder.matches(password,user.getPassword())) {
return HttpResult.error("密码不正确");
}
// 全部正确,则生成 token,返回给客户端
String token = tokenManager.createToken(username);
return token;
}
}
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