本次基于Spring Boot
整合了Spring Security
和Jwt
,可以解决前后端分离之后用户认证与授权的问题。在前后端还未分离的时候,对用户进行身份认证大约是这样的。
客户端客户端…服务端服务端…用户登录用户登录登录成功,用户信息存储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);
}
}
|
这个类是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
的配置类,可以配置token
的SECRET_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 |
Bearer
和token
之间有空格。获取到的token
也就是上面获取到的jwt
的值。
项目源码
项目源码地址:https://github.com/srcrs/security-jwt ,或者点我
参考链接
security-jwt整合视频教程
token与cookie的比较