目录

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 俗称令牌。

# 令牌类型

image-20211225180707107

先看一个 token 长什么样

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJrZWxlIiwiZXhwIjoxNjQwNDA0MzgzLCJjcmVhdGVkIjoxNjQwMzYxMTgzMjI0LCJhdXRob3JpdGllcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9OT1JNQUwifSx7ImF1dGhvcml0eSI6IlJPTEVfQURNSU4ifV19.BL_XFikpxXxzzH6hI_3lJgB7IWezyM4m33IISV6O5USeq5z1xuWf8rB02S-Rg8tdmzNBFVHXvZO-zgdFYE8mIQ
1

可以看到 token 是一个很长的字符串,字符之间通过「 . 」分隔符分为三个子串,每一个子串表示了一个功能块,总共有以下三个部分:JWT 头、有效载荷和签名。

# JWT头

JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示。

{
    "alg": "HS256",
    "typ": "JWT"
}
1
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)
1

在计算出签名哈希后,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
1
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 方法
}
1
2
3
4
5
6
7

和数据库对应的权限类 Role,记得加上 set 和 get 方法

public class Role {
    private Long id;
    private String roleName;

    // set 和 get 方法
}
1
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;
    }
}
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

# 认证授权工具类

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 无需删除,客户端扔掉即可。
    }
}
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

用户登出的工具类: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());
    }
}
1
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());
    }
}
1
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());
    }
}
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

授权拦截器: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;
    }
}
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

# 业务类

其他的类我就不写了,就是 service 层、mapper 层,至于数据库的三个张表(用户表、角色表、用户角色关联表),效果如图:

image-20211224192105286

提供 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}
1

最重要的是这个业务类,自定义类继承 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);
    }
}
1
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/**");
    }
}
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

# 测试类

编写一个 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;
    }
}
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
更新时间: 2024/01/17, 05:48:13
最近更新
01
JVM调优
12-10
02
jenkins
12-10
03
Arthas
12-10
更多文章>