spring boot下spring security结合token的使用小结

本文将详细介绍在spring boot环境中使用spring security实现token认证的方法和注意事项。

Maven依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

Spring Security 核心类

WebSecurityConfigurerAdapter

要利用Spring Security把安全的事情做了,最核心的就是继承这个WebSecurityConfigurerAdapter,根据自己的业务需要重新configure方法,示例代码如下:

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
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
private UserDetailsService userDetailsService;
@Value("${anonymous.path}")
private String[] ANONYMOUS_PATH;
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(this.userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}
@Override
public void configure(WebSecurity web) throws Exception {
if (ANONYMOUS_PATH.length > 0) {
web.ignoring().antMatchers(ANONYMOUS_PATH);
}
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 由于使用的是JWT,我们这里不需要csrf
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// 允许对于网站静态资源的无授权访问
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/layui/**",
"/dist/**",
"/**/*.js"
).permitAll()
//允许ajax的option请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 对于获取token的rest api要允许匿名访问
.antMatchers(
"/auth/login",
"/swagger-ui.html",
"/webjars/**",
"/swagger-resources/**",
"/*/api-docs",
"/configuration/**",
"/attachment/**",
"/system/encrypt",
"/actuator/**",
"/h2/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 添加JWT filter
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
// 禁用缓存
httpSecurity.headers().cacheControl();
//允许跨域
httpSecurity.headers().frameOptions().disable();
}
}
  • configure(HttpSecurity httpSecurity)里面的注释很全,主要做的事情是配置规则和参数。
  • configure(WebSecurity web)的目的是实现自定义的匿名访问授权。

OncePerRequestFilter

上面的配置类中声明了要使用authenticationTokenFilterBean进行权限验证,下面这个继承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
@SuppressWarnings("SpringJavaAutowiringInspection")
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUserDetailsService jwtUserDetailsService;
@Autowired
private UserService userService;
private String tokenHeader = "Authorization";
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
final String authToken = request.getHeader(this.tokenHeader);
if (authToken != null) {
String usercode = TokenUtils.getUsercodeFromToken(authToken);
logger.info("checking authentication " + usercode);
if (usercode != null && SecurityContextHolder.getContext().getAuthentication() == null) {
SystemUserModel user = userService.getUserByUsercode(usercode);
UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(usercode);
if (TokenUtils.validateToken(authToken, userDetails) && userService.isPermissionApi(user.getId(), request.getRequestURI(), request.getMethod())) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
request));
logger.info("authenticated user " + usercode + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
  • doFilterInternal是核心方法,在这里实现了token的提取和认证

UserDetailsService

UserDetailsService是Spring Security获取用户信息的核心类,配合使用的model是UserDetails,用这个类去存放用户信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class JwtUserDetailsService implements UserDetailsService {
private UserService userService;
@Override
public UserDetails loadUserByUsername(String usercode) throws UsernameNotFoundException {
SystemUserModel user = userService.getUserByUsercode(usercode);
if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", usercode));
} else {
String role = userService.getRole(usercode).getRolename();
return JwtUserFactory.create(user, role);
}
}
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}
  • loadUserByUsername是需要重写的方法,在这里写明如何加载用户

UserDetails

下面是UserDetails的一个示例,要点就是要实现UserDetails里的接口

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
public class JwtUser implements UserDetails {
private final String id;
private final String username;
private final String password;
private final String email;
private final Collection<? extends GrantedAuthority> authorities;
public JwtUser(
String id,
String username,
String password,
String email,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.password = password;
this.email = email;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@JsonIgnore
public String getId() {
return id;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}

Token 核心类

token这块的重点就是如何生成token,如何验证token,如何刷新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
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
@Component
public class TokenUtils {
public static final String CLAIM_KEY_USERNAME = "sub";
public static final String CLAIM_KEY_CREATED = "crt";
private static String secret;
private static Long expiration;
/**
* 从 Token 中获取 usercode
*/
public static String getUsercodeFromToken(String token) {
String usercode;
try {
final Claims claims = getClaimsFromToken(token);
usercode = claims.getSubject();
} catch (Exception e) {
usercode = e.toString();
}
return usercode;
}
/**
* 从 Token 中获取创建时间
*/
public static Date getCreatedDateFromToken(String token) {
Date created;
try {
final Claims claims = getClaimsFromToken(token);
created = Objects.equals(null, claims) ? null : new Date((Long) claims.get(CLAIM_KEY_CREATED));
} catch (Exception e) {
created = null;
}
return created;
}
/**
* 从 Token 中获取过时时间
*/
public static Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
private static Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成过期时间
*/
private static Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 验证 Token 是否过期
*/
private static Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
private static Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}
/**
* 生成 Token
*/
public static String generateToken(String usercode) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, usercode);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 生成 Token
*/
public static String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public static Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
final Date created = getCreatedDateFromToken(token);
return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
&& !isTokenExpired(token);
}
/**
* 刷新 Token
*/
public static 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;
}
/**
* 验证 Token
*/
public static Boolean validateToken(String token,SystemUserModel user) {
final String usercode = getUsercodeFromToken(token);
return (usercode.equals(user.getUsercode()) && !isTokenExpired(token));
}
public static Boolean validateToken(String token,UserDetails userDetails) {
final String usercode = getUsercodeFromToken(token);
return (usercode.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
@Value("${jwt.secret}")
public void setSecret(String secret) {
TokenUtils.secret = secret;
}
@Value("${jwt.expiration}")
public void setExpiration(Long expiration) {
TokenUtils.expiration = expiration;
}
}

END