SpringSecurity

SpringSecurity

初识

认证流程

  • 过滤器链与查看方式:

过滤器链与查看方法

  • 认证流程:

认证流程

准备工作

1. 必要依赖:

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
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- https://github.com/jwtk/jjwt#install -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
  • SpringSecurity 依赖添加以后会发现访问 controller 接口会自动跳转到 /login 接口弹出一个登录页面登录后才能进行访问,登录名称默认为 user 密码则被打印在了控制台中
  • 也可已通过配置文件指定用户名和密码:
1
2
3
4
5
spring:
security:
user:
name: user
password: 123456
  • 通过配置类配置用户(上方配置将无效):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean // 基于内存的用户存储
public UserDetailsService userDetailsService() {
UserDetails user = User.withUsername("user")
.password("{noop}123456")
.roles("USER")
.build();
UserDetails admin = User.withUsername("admin")
.password("{noop}123456")
.roles("ADMIN", "USER")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}

2. Redis 配置:

Redis 序列化配置:
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
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;

@Configuration
public class RedisConfig {

@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);

// 使用 fastJSON 作为 value 序列化器,并启用 autoType 特性
FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
serializer.setFastJsonConfig(new FastJsonConfig());

// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

template.setKeySerializer(stringRedisSerializer);
template.setValueSerializer(serializer);
// Hash 的 key 也采用 StringRedisSerializer 的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
template.setHashValueSerializer(serializer);

template.afterPropertiesSet();
return template;
}
}

IDE 没有正确识别 RedisConnectionFactory 类型的参数,导致参数报红报错。但是实际上 SpringBoot 在运行时会自动注入到 redisTemplate 方法中,因此在实际使用时并不会出现问题。

  • 方法上的 @ConditionalOnSingleCandidate 会检查容器中是否存在 RedisConnectionFactory 类型的 Bean,如果存在且只有一个,则会创建 RedisTemplate Bean。
Redis 缓存工具类:
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
@Component
public class RedisCache {
@Resource
private RedisTemplate redisTemplate;

/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value) {
redisTemplate.opsForValue().set(key, value);
}

/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}

/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout) {
return expire(key, timeout, TimeUnit.SECONDS);
}

/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}

/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}

/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key) {
return redisTemplate.delete(key);
}

/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public long deleteObject(final Collection collection) {
return redisTemplate.delete(collection);
}

/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList) {
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}

/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}

/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext()) {
setOperation.add(it.next());
}
return setOperation;
}

/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key) {
return redisTemplate.opsForSet().members(key);
}

/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}

/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key) {
return redisTemplate.opsForHash().entries(key);
}

/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
redisTemplate.opsForHash().put(key, hKey, value);
}

/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey) {
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}

/**
* 删除Hash中的数据
*
* @param key
* @param hkey
*/
public void delCacheMapValue(final String key, final String hkey) {
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.delete(key, hkey);
}

/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
return redisTemplate.opsForHash().multiGet(key, hKeys);
}

/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern) {
return redisTemplate.keys(pattern);
}
}

3. 统一响应体:

  • 响应结果枚举
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
@Getter
@AllArgsConstructor
public enum ResultEnum {
// 1xx 信息
CONTINUE(100, "Continue", "继续"),
SWITCHING_PROTOCOLS(101, "Switching Protocols", "切换协议"),
PROCESSING(102, "Processing", "处理中"),
// 2xx 成功
SUCCESS(200, "Success", "成功"),
CREATED(201, "Created", "已创建"),
ACCEPTED(202, "Accepted", "已接受"),
NON_AUTHORITATIVE_INFO(203, "Non-Authoritative Information", "非权威信息"),
NO_CONTENT(204, "No Content", "无内容"),
RESET_CONTENT(205, "Reset Content", "重置内容"),
PARTIAL_CONTENT(206, "Partial Content", "部分内容"),
MULTI_STATUS(207, "Multi-Status", "多状态"),
// 3xx 重定向
MULTIPLE_CHOICES(300, "Multiple Choices", "多种选择"),
MOVED_PERMANENTLY(301, "Moved Permanently", "永久移动"),
FOUND(302, "Found", "临时移动"),
SEE_OTHER(303, "See Other", "查看其他位置"),
NOT_MODIFIED(304, "Not Modified", "未修改"),
USE_PROXY(305, "Use Proxy", "使用代理"),
UNUSED(306, "Unused", "未使用"), // 已废弃,保留历史意义。
TEMP_REDIRECT(307, "Temporary Redirect", "临时重定向"),
// 4xx 客户端错误
BAD_REQUEST(400, "Bad Request", "错误请求"),
UNAUTHORIZED(401, "Unauthorized", "未授权"),
PAYMENT_REQUIRED(402, "Payment Required", "需要付费"), // 不常用。
FORBIDDEN(403, "Forbidden", "禁止"),
NOT_FOUND(404, "Not Found", "未找到"),
METHOD_NOT_ALLOWED(405, "Method Not Allowed", "方法不允许"),
NOT_ACCEPTABLE(406, "Not Acceptable", "不接受"),
PROXY_AUTH_REQUIRED(407, "Proxy Authentication Required", "需要代理认证"),
REQUEST_TIMEOUT(408, "Request Timeout", "请求超时"),
CONFLICT(409, "Conflict", "冲突"),
GONE(410, "Gone", "已删除"),
LENGTH_REQUIRED(411, "Length Required", "需要长度"),
PRECON_FAILED(412, "Precondition Failed", "前提条件失败"),
ENTITY_TOO_LARGE(413, "Request Entity Too Large", "请求实体过大"),
URI_TOO_LONG(414, "Request-URI Too Long", "请求URI过长"),
UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type", "不支持的媒体类型"),
RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable", "请求范围不符合"),
EXPECTATION_FAILED(417, "Expectation Failed", "期望失败"),
TEAPOT(418, "I'm a teapot", "我是一个茶壶"), // 开玩笑的状态码。
MISDIRECTED_REQUEST(421, "Misdirected Request", "错误的请求"),
UNPROCESSABLE_ENTITY(422, "Unprocessable Entity", "无法处理的实体"),
LOCKED(423, "Locked", "锁定"),
FAILED_DEPENDENCY(424, "Failed Dependency", "依赖失败"),
TOO_EARLY(425, "Too Early", "太早"),
UPGRADE_REQUIRED(426, "Upgrade Required", "需要升级"),
RETRY_WITH(449, "Retry With", "请重试"), // 微软扩展。
LEGAL_REASONS(451, "Unavailable For Legal Reasons", "因法律原因不可用"),
// 5xx 服务器错误
FAIL(500, "Internal Server Error", "内部服务器错误"),
NOT_IMPLEMENTED(501, "Not Implemented", "未实现"),
BAD_GATEWAY(502, "Bad Gateway", "错误网关"),
SERVICE_UNAVAILABLE(503, "Service Unavailable", "服务不可用"),
GATEWAY_TIMEOUT(504, "Gateway Timeout", "网关超时"),
HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version Not Supported", "HTTP版本不支持"),
VARIANT_NEGOTIATES(506, "Variant Also Negotiates", "变体协商"),
INSUFFICIENT_STORAGE(507, "Insufficient Storage", "存储不足"),
BANDWIDTH_EXCEEDED(509, "Bandwidth Limit Exceeded", "带宽超限"),
NOT_EXTENDED(510, "Not Extended", "未扩展"),
UNPARSEABLE_HEADERS(600, "Unparseable Response Headers", "无法解析的响应头"); // 非标准。

private final Integer code;
private final String msg;
private final String description;
}
  • 响应类
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
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class RespResult<T> {

private Integer code;
private String msg;
private T data;

public RespResult(ResultEnum resultEnum) {
this.code = resultEnum.getCode();
this.msg = resultEnum.getMsg();
}

public RespResult<T> code(Integer c) {
this.code = c;
return this;
}

public RespResult<T> msg(String m) {
this.msg = m;
return this;
}

public RespResult<T> data(T d) {
this.data = d;
return this;
}

public RespResult<T> setCodeAndMsg(ResultEnum resultEnum) {
this.code = resultEnum.getCode();
this.msg = resultEnum.getMsg();
return this;
}

public RespResult<T> setCodeAndMsg(Integer code, String msg) {
this.code = code;
this.msg = msg;
return this;
}

public static <T> RespResult<T> success() {
return new RespResult<>(ResultEnum.SUCCESS);
}

public static <T> RespResult<T> success(String msg) {
return new RespResult<T>(ResultEnum.SUCCESS).msg(msg);
}

public static <T> RespResult<T> success(T data) {
return new RespResult<T>(ResultEnum.SUCCESS).data(data);
}

public static <T> RespResult<T> success(String msg, T data) {
return new RespResult<T>(ResultEnum.SUCCESS).msg(msg).data(data);
}

public static <T> RespResult<T> fail() {
return new RespResult<>(ResultEnum.FAIL);
}

public static <T> RespResult<T> fail(String msg) {
return new RespResult<T>(ResultEnum.FAIL).msg(msg);
}

public static <T> RespResult<T> fail(T data) {
return new RespResult<T>(ResultEnum.FAIL).data(data);
}

public static <T> RespResult<T> fail(String msg, T data) {
return new RespResult<T>(ResultEnum.FAIL).msg(msg).data(data);
}

public static <T> RespResult<T> result() {
return new RespResult<>();
}

public static <T> RespResult<T> result(Integer code) {
return new RespResult<T>().code(code);
}

public static <T> RespResult<T> result(String msg) {
return new RespResult<T>().msg(msg);
}

public static <T> RespResult<T> result(Integer code, String msg) {
return new RespResult<T>().code(code).msg(msg);
}

public static <T> RespResult<T> result(Integer code, String msg, T data) {
return new RespResult<T>().code(code).msg(msg).data(data);
}

public static <T> RespResult<T> enumResult(ResultEnum resultEnum) {
return new RespResult<>(resultEnum);
}

public static <T> RespResult<T> enumResult(ResultEnum resultEnum, T data) {
return new RespResult<T>(resultEnum).data(data);
}
}
  • 响应数据工具类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class WebUtils {
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}

4. JWT 工具类:

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
public class JwtUtil {
public static final Long JWT_TTL = 60 * 60 * 1000L; // 一个小时
public static final String JWT_ISSUER = "ruoxijun";
public static final String JWT_KEY = "ruoxijun+ruoxijun+ruoxijun+ruoxijun+ruoxijun+ruoxijun";

public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}

public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());
return builder.compact();
}

public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);
return builder.compact();
}

private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题可以是JSON数据
.setIssuer(JWT_ISSUER) // 签发者
.setIssuedAt(now) // 签发时间
.signWith(secretKey, signatureAlgorithm) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}

public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(jwt)
.getBody();
}

public static SecretKey generalKey() {
return Keys.hmacShaKeyFor(JwtUtil.JWT_KEY.getBytes(StandardCharsets.UTF_8));
}

public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
}

数据库登录

  • 由认证流程可知 SpringSecurity 通过 UserDetailsServiceloadUserByUsername 方法查询用户后返回 UserDetails 用户信息对象。

1. UserDetails:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;
@Override
public String getPassword() { return user.getPassword(); }
@Override
public String getUsername() { return user.getUsername(); }

@Override// 获取权限信息
public Collection<? extends GrantedAuthority> getAuthorities() { return null; }

@Override // 是否没有过期
public boolean isAccountNonExpired() { return true; }
@Override // 是否未锁定
public boolean isAccountNonLocked() { return true; }
@Override // 凭据是否未过期
public boolean isCredentialsNonExpired() { return true; }
@Override // 是否可用
public boolean isEnabled() { return true; }
}
  • 不实现 UserDetails 类,可以采用 User.withUsername... 方式来生成 UserDetails 对象。

2. UserDetailsService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class UserDetailsServiceImp implements UserDetailsService {
@Resource
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername, username);
User user = userMapper.selectOne(queryWrapper);
if (Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}

// todo 查询用户对应权限

return new LoginUser(user);
}
}
  • 这时想要使用数据库的用户账号密码登录还需要在用户 数据库的密码前面 添加 {noop} ,之后就可以使用该账号密码登录了

JWT 登录

登录流程

  1. 自定义 /login 登录接口,并在 SpringSecurity 配置文件中让它放行
  2. 在接口中通过 AuthenticationManagerauthenticate 进行用户认证
    • AuthenticationManager 需要在配置文件中注入容器
    • authenticate 需要一个 Authentication 接口对象我们使用 UsernamePasswordAuthenticationToken 认证成功会返回这个 Authentication 对象否则为 null
  3. 认证成功后生成 JWT 并存入 Redis ,最后将 JWT(token) 响应给客户端
  4. 自定义 JWT 认证过滤器,之后用户携带 token 访问接口时通过解析 token 获取用户信息,并将信息出入 SecurityContextHolder 中(仅此次请求有效)
    • 继承 OncePerRequestFilter 过滤器确保一次请求仅被调用一次,通常一个请求会经过多个过滤器,如果没有限制一个过滤器可能被调用多次

密码加密处理:

  • 在 SpringSecurity 中,如果使用的是 明文密码 ,则需要在密码前添加 {noop} 前缀,这是因为SpringSecurity 默认使用加密密码来保护用户密码

  • SpringSecurity 中最常用的是 BCryptPasswordEncoder 使用 bcrypt 哈希算法来加密密码

  • 在 SpringSecurity 配置文件中把 BCryptPasswordEncoder 注入容器(@Bean),SpringSecurity 则会使用该 PasswordEncoder 进行密码校验

BCryptPasswordEncoder 常用方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
@Resource
BCryptPasswordEncoder bCryptPasswordEncoder;

@Test
void contextLoads() {
// BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = "123456";
// 密码加密
String encode = bCryptPasswordEncoder.encode(password);
// 密码校验
boolean matches = bCryptPasswordEncoder.matches(password, encode);
System.out.println(encode + " <-=-> " + matches);
}

LoginService

  • controller 登录接口 /login 和 LoginService 接口类请自行实现,这里是登录业务实现:
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
@Service
public class LoginServiceImpl implements LoginService {
@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisCache redisCache;

@Override
public RespResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
user.getUsername(), user.getPassword()); // 传入用户名和密码
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if(Objects.isNull(authenticate)){ // 认证未通过
throw new RuntimeException("用户名或密码错误");
}
// 获取 UserDetails 对象,使用 userId 生成 token
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
// authenticate 存入 redis
redisCache.setCacheObject("login:"+userId, loginUser);
// token 响应给前端
HashMap<String,String> map = new HashMap<>();
map.put("token",jwt);
return RespResult.success("登录成功", map);
}

@Override
public RespResult logout() {
Authentication authentication = SecurityContextHolder
.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userid = loginUser.getUser().getId();
redisCache.deleteObject("login:" + userid);
return RespResult.success("注销成功");
}
}

JwtAuthenticationFilter

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
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Resource
private RedisCache redisCache;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取 token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
filterChain.doFilter(request, response);
return;
}
// 解析 token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
// 从 redis 中获取用户信息
String redisKey = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if (Objects.nonNull(loginUser)) {
// 存入 SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(
loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
}

SecurityConfig

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
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Resource
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").anonymous()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.cors(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(conf -> conf
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.build();
}

@Bean // 密码加密方式
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean // 获取 AuthenticationManager(认证管理器),登录时认证使用
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}

访问权限

在 SpringSecurity 中,会使用默认的 FilterSecurityInterceptor 过滤器来进行权限校验。它会从SecurityContextHolder 中获取当前 Authentication ,并获取其中包含的权限信息,以判断当前用户是否有权访问当前资源。

准备工作

  • 在 SpringSecurity 的配置类 SecurityConfig 上添加如下注解:
1
@EnableMethodSecurity(prePostEnabled = true)

它启用方法级的安全约束, prePostEnabled=true 表示启用预验证(pre:方法执行前校验)和后验(post:方法执行后校验)拦截器

  • 假如给 controller /hello 接口方法上方添加如下权限校验注解,它表示执行该方法前它会校验当前用户是否有 admin 权限
1
2
@PreAuthorize("hasAuthority('admin')") // 方法执行之前校验
// @PreAuthorize("hasAuthority('admin')") // 方法执行之后校验

获取权限

1. 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
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;

private List<String> permissions;

@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;

public LoginUser(User user, List<String> permissions){
this.user = user;
this.permissions = permissions;
}

@Override
public String getPassword() { return user.getPassword(); }

@Override
public String getUsername() { return user.getUsername(); }

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (Objects.nonNull(authorities)) {
return authorities;
}
authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}

@Override // 是否没有过期
public boolean isAccountNonExpired() { return true; }

@Override // 是否未锁定
public boolean isAccountNonLocked() { return true; }

@Override // 凭据是否未过期
public boolean isCredentialsNonExpired() { return true; }

@Override // 是否可用
public boolean isEnabled() { return true; }
}

2. UserDetailsService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class UserDetailsServiceImp implements UserDetailsService {

@Resource
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername, username);
User user = userMapper.selectOne(queryWrapper);
if (Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
// 将用户对象与用户权限封装到 UserDetails 中
// 此处权限应是用户数据库中查询到的权限
LoginUser loginUser = new LoginUser(user, Arrays.asList("admin"));
return loginUser;
}
}

其它校验

校验方法:

1
2
3
4
5
6
@PreAuthorize("hasAuthority('admin')") // 校验某个权限
@PreAuthorize("hasAnyAuthority('admin', 'admin2')") // 校验多个权限

// 与上类似
@PreAuthorize("hasRole('admin2')")
@PreAuthorize("hasAnyRole('admin', 'admin2')")

自定义校验:

  • 定义一个自定义校验方法的通用接口:
1
2
3
public interface PermissionService {
boolean checkPermission(String authority);
}
  • 实现校验接口与校验方法:
1
2
3
4
5
6
7
8
9
10
11
12
@Service("psi")
public class PermissionServiceImpl implements PermissionService {
public boolean checkPermission(String authority) {
// 获取用户权限
Authentication authentication = SecurityContextHolder
.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
// 校验权限
return permissions.contains(authority);
}
}
  • 使用校验方法:
1
2
// @psi 表示 bean 在容器中的名称加上 @ 前缀
@PreAuthorize("@psi.checkPermission('admin')")

配置校验:

1
2
3
4
5
6
7
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").anonymous()
// 需要 admin 权限才能访问 /hello 接口
.requestMatchers("/hello").hasAuthority("admin")
.anyRequest().authenticated()
)
  1. 如果配置和注解包含相同接口,先会校验配置类中的权限再校验注解的权限
  2. 配置中的权限校验发生授权异常时不会被全局异常处理器捕获,而是被授权异常处理器捕获。而到校验注解方式授权异常时会被全局异常捕获

异常处理

认证异常

  • 在认证过程中出现的异常会被封装成 AuthenticationException 然后调用 AuthenticationEntryPoint 对象的方法去进行异常处理
1
2
3
4
5
6
7
8
9
10
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
RespResult<Object> respResult = RespResult.result(
HttpStatus.UNAUTHORIZED.value(), "身份认证失败");
String json = JSON.toJSONString(respResult);
WebUtils.renderString(response, json);
}
}

授权异常

  • 授权过程中出现的异常会被封装成 AccessDeniedException 然后调用 AccessDeniedHandler 对象的方法去进行异常处理
1
2
3
4
5
6
7
8
9
10
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
RespResult<Object> respResult = RespResult.result(
HttpStatus.FORBIDDEN.value(), "用户权限不足");
String json = JSON.toJSONString(respResult);
WebUtils.renderString(response, json);
}
}

添加配置

  • 在 SecurityConfig 中获取认证与授权异常处理器:
1
2
3
4
@Resource
private AuthenticationEntryPoint authenticationEntryPoint;
@Resource
private AccessDeniedHandler accessDeniedHandler;
  • 增加如下配置:
1
2
3
4
5
6
7
http
.exceptionHandling(conf -> conf
// 授权异常处理
.accessDeniedHandler(accessDeniedHandler)
// 认证异常处理
.authenticationEntryPoint(authenticationEntryPoint)
)

全局异常

如果配置了 @ControllerAdvice 全局异常处理,其中处理的异常范围若包括了 AccessDeniedException 那么 注解权限 授权异常将被它拦截 SpringSecurity 不再处理,因此我们需要在全局异常中处理注解校验的授权异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ResponseBody
@ControllerAdvice
public class GlobalExceptionHandler {
// SpringSecurity 授权异常处理
@ExceptionHandler(AccessDeniedException.class)
public RespResult<String> handlerAccessDeniedException(Exception e) {
return RespResult.result(HttpStatus.FORBIDDEN.value(), "权限不足");
}

// 全局异常处理
@ExceptionHandler(Exception.class)
public RespResult<String> handlerException(Exception e) {
return RespResult.fail("程序异常", e.getMessage());
}
}

其它处理器

认证成功

1
2
3
4
5
6
7
@Component
public class SGSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
System.out.println("认证成功了");
}
}

认证失败

1
2
3
4
5
6
7
@Component
public class SGFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) {
System.out.println("认证失败了");
}
}

登出成功

1
2
3
4
5
6
7
@Component
public class SGLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
System.out.println("注销成功");
}
}

添加配置

  • 注入:
1
2
3
4
5
6
@Resource
private AuthenticationSuccessHandler successHandler;
@Resource
private AuthenticationFailureHandler failureHandler;
@Resource
private LogoutSuccessHandler logoutSuccessHandler;
  • 配置:
1
2
3
4
5
6
7
8
9
10
11
http
.formLogin(conf -> conf
// 登录成功处理
.successHandler(successHandler)
// 登录失败处理
.failureHandler(failureHandler)
)
.logout(conf -> conf
// 登出成功处理
.logoutSuccessHandler(logoutSuccessHandler)
)

完整配置

  • 待完善
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
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfiguration {
@Resource
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Resource
private AuthenticationEntryPoint authenticationEntryPoint;
@Resource
private AccessDeniedHandler accessDeniedHandler;
@Resource
private AuthenticationSuccessHandler successHandler;
@Resource
private AuthenticationFailureHandler failureHandler;
@Resource
private LogoutSuccessHandler logoutSuccessHandler;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
// 允许匿名访问
.requestMatchers("/api/auth/**").anonymous()
// 权限控制
.requestMatchers("/hello").hasAuthority("admin")
.anyRequest().authenticated() // 其他请求需要认证
)
.exceptionHandling(conf -> conf
// 授权异常处理
.accessDeniedHandler(accessDeniedHandler)
// 认证异常处理
.authenticationEntryPoint(authenticationEntryPoint)
)
.formLogin(conf -> conf
// 登录成功处理
.successHandler(successHandler)
// 登录失败处理
.failureHandler(failureHandler)
)
.logout(conf -> conf
// 登出成功处理
.logoutSuccessHandler(logoutSuccessHandler)
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.cors(Customizer.withDefaults()) // 跨域配置
.csrf(AbstractHttpConfigurer::disable) // 关闭 csrf
// 关闭 session
.sessionManagement(conf -> conf
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.build();
}

@Bean // 密码加密方式
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean // 获取 AuthenticationManager(认证管理器),登录时认证使用
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}