Spring Security整合Jwt

本次基于Spring Boot整合了Spring SecurityJwt,可以解决前后端分离之后用户认证与授权的问题。在前后端还未分离的时候,对用户进行身份认证大约是这样的。

客户端…服务端…用户登录用户登录登录成功,用户信息存储session登录成功,用户信息存储session返回登录状态,并发送cookie返回登录状态,并发送cookie将服务端发来的cookie存储将服务端发来的cookie存储携带cookie,发送请求携带cookie,发送请求Viewer does not support full SVG 1.1

这种缺点就是身份信息需要客户端和服务器同时存储,当用户基数很大的时候,需要大量的内存来解决这个问题。

在前后端分离之后,基于token的用户身份认证大约是这样的。

客户端…服务端…用户登录用户登录登录成功,颁发token登录成功,颁发token将服务端发来的token存储将服务端发来的token存储携带token,发送请求携带token,发送请求Viewer does not support full SVG 1.1

这种好处是token只需要存储到客户端,服务端只需要对发来的请求中验证token的有效性。

本次便使用基于token的方式,结合spring security进行一次简单的身份认证与授权。

# 相关版本信息

名称 版本
IDEA商业版 2020.1
JDK JDK1.8
Maven 3.5.4
Windows 家庭版1903

# 项目结构

 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
.
├── .idea
├── src
│   └── main
|       ├── java
|       |   └── com
|       |       └── example
|       |           ├── controller
|       |           |   └── HelloResource.java
|       |           ├── filters
|       |           |   └── JwtRequestFilter.java
|       |           ├── model
|       |           |   ├── AuthenticationRequest.java
|       |           |   └── AuthenticationResponse.java
|       |           ├── security
|       |           |   ├── MyUserDetailsService.java
|       |           |   └── SecurityConfigurer.java
|       |           ├── utils
|       |           |   └── JwtUtil.java     
|       |           └── Application.java
│       └── resources
│           └── application.properties
├── test
├── target
├── pom.xml
└── security-jwt.iml 

# 在pom.xml添加相关jar包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

# 创建Application.java

这个其实就是Spring Boot的入口文件,名称不一样也没事,内容也没有改动。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

# 创建SecurityConfigurer.java

这个类是Spring Security的配置类,Spring Boot提倡去掉配置文件,用配置类来代替,道理都差不多,我还是熟悉xml一些。

 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
package com.example.security;


import com.example.filters.JwtRequestFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@EnableWebSecurity
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Autowired
    JwtRequestFilter jwtRequestFilter;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService).passwordEncoder(bCryptpasswordEncoder());
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/authenticate")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public BCryptPasswordEncoder bCryptpasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

# 创建MyUserDetailsService.java

这个类是通过传来用户的username,返回一个用户对象,这里为了简便没有从数据库进行查询,以后改成从数据库访问用户信息,直接在这里查询并返回一个用户就行了。

这里密码采用了BCR加密。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package com.example.security;

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;

@Service
public class MyUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return new User("foo",new BCryptPasswordEncoder().encode("foo"),new ArrayList<>());
    }
}

# 创建JwtUtil.java

这个是Jwt的配置类,可以配置tokenSECRET_KEY,到期时间等等,更重要的作用是可以生成一个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
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
package com.example.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtUtil {

    private String SECRET_KEY = "secret";

    public String extractUsername(String token){
        return extractClaim(token, Claims::getSubject);
    }

    public Date extractExpiration(String token){
        return extractClaim(token,Claims::getExpiration);
    }

    public <T> T extractClaim(String token, Function<Claims,T> claimsResolver){
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    public Claims extractAllClaims(String token){
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
    }

    public Boolean isTokenExpired(String token){
        return extractExpiration(token).before(new Date());
    }

    public String generateToken(UserDetails userDetails){
        Map<String,Object> claims = new HashMap<>();
        return createToken(claims,userDetails.getUsername());
    }

    private String createToken(Map<String,Object> claims,String subject){
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis()+100*60*60*10))
                .signWith(SignatureAlgorithm.HS256,SECRET_KEY)
                .compact();
    }
    public Boolean validateToken(String token,UserDetails userDetails){
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername())) && (!isTokenExpired(token));
    }

}

# 创建AuthenticationRequest.java

这个类的作用是将登录请求信息封装成一个对象,登录的对象是用户,日后添加访问数据库便有了用户类,就不再需要这个类了。

 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
package com.example.model;

public class AuthenticationRequest {
    private String username;
    private String password;

    public AuthenticationRequest() {
    }

    public AuthenticationRequest(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

# 创建AuthenticationResponse.java

这个类的作用同样是将信息封装成类,只不过这次是发送出去,即响应请求,我觉得封装成一个Map要更好一点,省的多创建一个类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.example.model;

public class AuthenticationResponse {
    private final String jwt;

    public AuthenticationResponse(String jwt) {
        this.jwt = jwt;
    }

    public String getJwt() {
        return jwt;
    }
}

# 创建JwtRequestFilter.java

这个类检查token是否有效,继承了OncePerRequestFilter类,简单翻译为一次请求的过滤链,也就是说,每次请求都需要这条过滤链的验证,通过了就可以放行,不通过就干掉。

 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
package com.example.filters;

import com.example.security.MyUserDetailsService;
import com.example.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    MyUserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            if(jwtUtil.validateToken(jwt,userDetails)) {
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails,null,userDetails.getAuthorities());
                usernamePasswordAuthenticationToken
                        .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        chain.doFilter(request,response);
    }
}

仅仅写此类还是没有作用的,还需要添加到Spring Security的过滤链中,具体是这个方法http.addFilterBefore(),这个应该改是在Security之前,也有添加到之后的方法,具体有四个相应的方法。我知道自己写的过滤链需要添加上,为什么后面要加一个class类,不太明白。

1
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

# 创建HelloResource.java

这个类就没啥说的了,接收和响应用户的请求。

 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
package com.example.controller;

import com.example.security.MyUserDetailsService;
import com.example.model.AuthenticationRequest;
import com.example.model.AuthenticationResponse;
import com.example.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

@RestController
public class HelloResource {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Autowired
    private JwtUtil jwtTokenUtil;

    @RequestMapping("/hello")
    public String hello() {
        return "Hello World";
    }

    @PostMapping("/authenticate")
    public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
        try{
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            authenticationRequest.getUsername(),
                            authenticationRequest.getPassword()
                    )
            );
        } catch (BadCredentialsException e) {
            throw new Exception("Incorrect username or password",e);
        }
        final UserDetails userDetails = myUserDetailsService
                .loadUserByUsername(authenticationRequest.getUsername());

        final String jwt = jwtTokenUtil.generateToken(userDetails);

        return ResponseEntity.ok(new AuthenticationResponse(jwt));
    }
}

# 测试

至此,已经完成了完成了所有的代码,现在进行测试。推荐使用postman

首先访问/authenticate接口,获取到token,然后携带token访问/hello获取到正确的信息。

注意一下,下面的接口路径需要自己添加前缀http://localhost:8080/,根据自己实际情况修改。

# 登录验证接口

  • 请求路径:authenticate
  • 请求方法:post
  • 请求参数
参数名 参数说明 备注
username 用户名 不能为空
password 密码 不能为空
  • 请求头
参数名 备注
content-type application/json header
  • 响应参数
参数名 参数说明 备注
jwt 令牌 基于 jwt 的令牌
  • 响应数据
1
2
3
{
    "jwt": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJmb28iLCJleHAiOjE1OTU2MDQ5NTIsImlhdCI6MTU5NTYwMTM1Mn0.Lk1v9FUCNLlnYmZzEFIcMB9nYPcQgYCoxv2Mg_jklpo"
}

# 携带token获取信息

  • 请求路径:hello

  • 请求方法:get

  • 请求参数

  • 请求头

参数名 备注
content-type application/json header
Authorization Bearer + 获取到的token header

Bearertoken之间有空格。获取到的token也就是上面获取到的jwt的值。

  • 响应参数

  • 响应数据

1
Hello World

# 项目源码

项目源码地址:https://github.com/srcrs/security-jwt ,或者点我

# 参考链接

security-jwt整合视频教程

token与cookie的比较

Licensed under CC BY-NC-SA 4.0