0%

Spring Boot 中使用 Spring Security + JWT 构建身份认证系统

权限控制是非常常见的功能,在各种后台管理里权限控制更是重中之重。在 Spring Boot 中使用 Spring Security 构建权限系统是非常轻松和简单的。Spring Security 是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

添加依赖

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

UserDetails

按照官方文档的说法,为了定义我们自己的认证管理,我们可以添加 UserDetailsService, AuthenticationProvider, AuthenticationManager 这种类型的 Bean。实现的方式有多种,这里我选择最简单的一种(因为本身我们这里的认证授权也比较简单)通过定义自己的 UserDetailsService 从数据库查询用户信息,至于认证的话就用默认的。

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
/**
* Author: vincent
* Date: 2018-06-12 16:50:00
* Comment:
*/

public class User implements UserDetails {

private Integer id;
private String username;
private String password;
private Date lastPasswordResetDate;
private List<Role> roles = new ArrayList<>();

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

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

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

@JsonIgnore
public List<Role> getRoles() {
return roles;
}

public void setRoles(List<Role> roles) {
this.roles = roles;
}

@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(Role::getName).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}

@Override
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return username;
}

@Override
@JsonIgnore
public boolean isAccountNonExpired() {
return true;
}

@Override
@JsonIgnore
public boolean isAccountNonLocked() {
return true;
}

@Override
@JsonIgnore
public boolean isCredentialsNonExpired() {
return true;
}

@Override
@JsonIgnore
public boolean isEnabled() {
return true;
}

@JsonIgnore
public Date getLastPasswordResetDate() {
return lastPasswordResetDate;
}

public void setLastPasswordResetDate(Date lastPasswordResetDate) {
this.lastPasswordResetDate = lastPasswordResetDate;
}
}

开启权限验证

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
/**
* Author: vincent
* Date: 2018-06-12 17:02:00
* Comment:
* @EnableWebSecurity 开启权限验证注解
* @EnableGlobalMethodSecurity(prePostEnabled = true) 开启方法级别的权限验证
*/

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

/**
* 因为我们实现了 UserDetailsService,Spring 会自动在项目中查找它的实现类,
* 这里注入的也自然是我们自定义的 UserDetailsServiceImpl 类
*/
@Resource
private UserDetailsService userDetailsService;

@Resource
private AuthenticationTokenFilter authenticationTokenFilter;

/**
* 设置我们自己实现的的 UserDetailsService,并设置密码的加密方式,加密方式不是必须的,也就是说,这里是可以明文密码
* @param authenticationManagerBuilder
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(this.userDetailsService).passwordEncoder(passwordEncoder());
}

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// 因为我们是基于 token 进行验证的,所以 csrf 和 session 我们这里都不需要
httpSecurity.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()

// 获取 token 的接口所有人都可以访问
.antMatchers("/authentication/**").permitAll()

// 除了上面的接口,其它接口都必须鉴权认证
.anyRequest().authenticated();
httpSecurity.headers().cacheControl();

// 将我们实现的过滤器添加进去,方法名可以很轻松的看出来是前置过滤
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}

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

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

集成 JWT

想要将 JWT(Json Web Token) 在 spring boot 中集成起来,需要创建一个 filter,继承自 OncePerRequestFilter。
并且将它添加到 WebSecurityConfig,需要注意的是,这个过滤器是作为权限验证,必须添加到 before 集合中。

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
/**
* Author: vincent
* Date: 2018-06-12 17:13:00
* Comment:
*/

@Component
public class AuthenticationTokenFilter extends OncePerRequestFilter {

@Resource
private UserDetailsService userDetailsService;

@Resource
private TokenUtil tokenUtil;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorization = request.getHeader(AUTHORIZATION);
if (!StringUtils.isEmpty(authorization) && authorization.startsWith(TokenUtil.TOKEN_PREFIX)) {
String token = authorization.substring(TokenUtil.TOKEN_PREFIX.length());
String username = tokenUtil.getUsernameFromToken(token);
if (!StringUtils.isEmpty(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (tokenUtil.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(request, response);
}
}
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
/**
* Author: vincent
* Date: 2018-06-12 17:17:00
* Comment:
*/

@Component
public class TokenUtil implements Serializable {
public static final String TOKEN_PREFIX = "Bearer ";

static final String CLAIM_KEY_USERNAME = "sub";
static final String CLAIM_KEY_CREATED = "iat";

private static final long serialVersionUID = -3301605591108950415L;

private Clock clock = DefaultClock.INSTANCE;

@Value("${jwt.secret}")
private String secret;

@Value("${jwt.expiration}")
private Long expiration;

public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}

public Date getIssuedAtDateFromToken(String token) {
return getClaimFromToken(token, Claims::getIssuedAt);
}

public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}

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

private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}

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

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

private Boolean ignoreTokenExpiration(String token) {
return false;
}

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

private String doGenerateToken(Map<String, Object> claims, String subject) {
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);

return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}

public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
final Date created = getIssuedAtDateFromToken(token);
return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
&& (!isTokenExpired(token) || ignoreTokenExpiration(token));
}

public String refreshToken(String token) {
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);

final Claims claims = getAllClaimsFromToken(token);
claims.setIssuedAt(createdDate);
claims.setExpiration(expirationDate);

return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}

public Boolean validateToken(String token, UserDetails userDetails) {
if (userDetails instanceof User) {
User user = (User) userDetails;
final String username = getUsernameFromToken(token);
final Date created = getIssuedAtDateFromToken(token);
return (
username.equals(user.getUsername())
&& !isTokenExpired(token)
&& !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())
);
} else {
System.out.println("类型转换失败!");
return false;
}
}

private Date calculateExpirationDate(Date createdDate) {
return new Date(createdDate.getTime() + expiration * 1000);
}
}

UserDetailsService

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
/**
* Author: vincent
* Date: 2018-06-12 16:49:00
* Comment: 这个接口只定义了一个方法 loadUserByUsername,顾名思义,就是提供一种从用户名可以查到用户并返回的方法。
* 注意,不一定是数据库,文本文件、xml文件等等都可能成为数据源,这也是为什么 Spring 提供这样一个接口的原因:保证你可以采用灵活的数据源。
*/

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

@Resource
private UserMapper userMapper;

@Resource
private RoleMapper roleMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.findByUsername(username);
if (user == null)
throw new UsernameNotFoundException("Cannot find user with username, username = " + username);
user.setRoles(roleMapper.selectByUserId(user.getId()));
return user;
}
}