SpringSecurity+JWT结合Vue在前后端分离下的权限控制

SpringSecurity+JWT结合Vue在前后端分离下的权限控制

前言

再构建一个前后端分离项目的时候。我们之前在前后端耦合情况下使用的Security配置会和前后端分离的情况下有一些较大的区别。

就比如在前后端耦合的情况下,所有页面。都是由后端来处理,这样的权限控制较为简易。而且不会出现跨域的问题。按目前结合vue进行前后端分离是当前技术的主流。很多公司和业务都是按照着这个的标准进行的,那么以往的权限控制就不能完全适用于现在了。

这一段设计经过了我的一些思考。当我在初步的使用vue的时候。我还按照原来的配置去配置security。结果发现这样的权限控制并没有任何效果。起初以为是跨域问题,结果在解决了跨域问题之后,才真正明白了,他们启动的并不是同一个端口。那么我在后端配置的权限控制和前端不同端口的情况下怎么会生效呢?

于是在查阅了网上的各种资料之后,我找到了一种方法,使用jwt配合security权限控制来进行前后端之间的交流,这样即使是在不同端口的情况下也能实现权限的控制。并且可以将jwt所认证的信息存储在当地的cookie当中。即使用户退出的浏览器在打开浏览器时也不用再次进行登录去认证。

准备工作

首先必不可说的肯定是依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
        <!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>


<!-- spring security 权限管理依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

因为在进行前后端分离时,需要用到一个自定义的类型去进行交互。当然这是基本常识,在此也不多讲述:

这是一个用于交互的实体,前后端用这个类来进行交互。。

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
@Data
public class Result implements Serializable {

private int code; // 200是正常,非200表示异常
private String msg;
private Object data;

public static Result succ(Object data) {
return succ(200, "操作成功", data);
}

public static Result succ(int code, String msg, Object data) {
Result r = new Result();
r.setCode(code);
r.setMsg(msg);
r.setData(data);
return r;
}

public static Result fail(String msg) {
return fail(400, msg, null);
}

public static Result fail(String msg, Object data) {
return fail(400, msg, data);
}

public static Result fail(int code, String msg, Object data) {
Result r = new Result();
r.setCode(code);
r.setMsg(msg);
r.setData(data);
return r;
}

}

我们还需要定义一个用户提交登陆信息使用的类

1
2
3
4
5
6
7
8
9
@Data
public class LoginDto implements Serializable {

@NotBlank(message = "昵称不能为空")
private String username;

@NotBlank(message = "密码不能为空")
private String password;
}

重点

后端需要解决一些跨域问题,这是一个跨域解决方案的一个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 解决跨域问题
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}

}

开始配置我们的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
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
MyUserDetailsService myUserDetailsService; //自行编写的持久层方法,这里我用的是mybatis

public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtTokenFilter();
//需要自定义一个JWT拦截器,下面会提及
}

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

@Override
protected void configure( AuthenticationManagerBuilder auth ) throws Exception {
auth.userDetailsService( myUserDetailsService ).passwordEncoder( new BCryptPasswordEncoder() );
}//将我们的持久层方法注入到这个security里面,之前的文章说过了

@Override
protected void configure( HttpSecurity httpSecurity ) throws Exception {

httpSecurity.csrf()
.disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.antMatchers(HttpMethod.POST, "/register").permitAll()
.antMatchers(HttpMethod.POST).authenticated()
.antMatchers(HttpMethod.PUT).authenticated()
.antMatchers(HttpMethod.DELETE).authenticated()
.antMatchers(HttpMethod.GET).authenticated()
.and()// 开启跨域
.cors()

//配置拦截
;

httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
//自定义拦截方法
httpSecurity.headers().cacheControl();

}

}
  • MyUserDetailsService:这个不用说,就是我们自己定义的获取数据库用户信息的方法。接着使用: auth.userDetailsService( myUserDetailsService ).passwordEncoder( new BCryptPasswordEncoder() ); 将他住到我们的security权限认证当中。
  • JwtTokenFilter:这是我们的拦截器。我们需要对验证的每一个Token.进行验证,判断它的过期时间等等。我们需要把这个拦截器一块注入到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
@Component
public class JwtTokenFilter extends OncePerRequestFilter {

@Qualifier("myUserDetailsService")
@Autowired
private UserDetailsService userDetailsService;

@Autowired
private JwtTokenUtil jwtTokenUtil;

//这里看起来复杂,实际就是获取从前端返回的信息,比如username,然后进行数据库查找,然后进行将找到的用户进行加密形成一个Token,将一个信息传递回security
@Override
protected void doFilterInternal ( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

String authHeader = request.getHeader( Const.HEADER_STRING );
if (authHeader != null && authHeader.startsWith( Const.TOKEN_PREFIX )) {
final String authToken = authHeader.substring( Const.TOKEN_PREFIX.length() );
String username = jwtTokenUtil.getUsernameFromToken(authToken);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}

接着这一个认证拦截器又用到了JWT的工具类:这一个工具,通常用于生成JWT,和获取各种JWT的信息,其中用到了我们spring中的依赖注的JWT源码,但我们还需要根据他所给的方法和API,进行自己的改写和判断:

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
104
105
106
107
108
109
//jwt的各种方法,写法比较固定

@Component
public class JwtTokenUtil implements Serializable {

private static final long serialVersionUID = -5625635588908941275L;

private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";

public String getUsernameFromToken(String token) {
String username;
try {
final Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}

public Date getCreatedDateFromToken(String token) {
Date created;
try {
final Claims claims = getClaimsFromToken(token);
created = new Date((Long) claims.get(CLAIM_KEY_CREATED));
} catch (Exception e) {
created = null;
}
return created;
}

public Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}

private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey( Const.SECRET )
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}

private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + Const.EXPIRATION_TIME * 1000);
}

private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}

private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}

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

String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, Const.SECRET )
.compact();
}

public Boolean canTokenBeRefreshed(String token) {
return !isTokenExpired(token);
}

public String refreshToken(String token) {
String refreshedToken;
try {
final Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}

public Boolean validateToken(String token, UserDetails userDetails) {
User user = (User) userDetails;
final String username = getUsernameFromToken(token);
return (
username.equals(user.getUsername())
&& !isTokenExpired(token)
);
}
}

在此,这种工作大致上就完成了,总体而言并不复杂。就是把JWT和security结合在一起。

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@PostMapping("/login")
public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response) {


System.out.println("登录");
String jwt= authService.login( loginDto.getUsername(), loginDto.getPassword() );
User user= userService.findByUsername(loginDto.getUsername()).get(0);
response.setHeader("Authorization",jwt);//放置响应头中
response.setHeader("Access-control-Expose-Headers","Authorization");

return Result.succ(MapUtil.builder()
.put("id", user.getId())
.put("username", user.getUsername())
.map());

}

使用postman或者是vue的axios进行测试,图床崩了暂时就不搞图了