Spring Security(一):用户授权实战
Spring Security(一):用户授权实战
这一篇文章主要循序渐进的总结一下使用Spring Security进行jwt
登录的实战。
默认授权(入门案例)
代码
创建SpringBoot项目
引入Spring Security 依赖
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>启动项目之后会自动拦截跳转登陆页面,用户名默认是
user
,密码在后台每次都会动态生成。
简单原理
Spring Security的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案主要的过滤器。
UsernamePasswordAuthenticationFilter
: 负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。- **
ExceptionTranslationFilter
**:处理过滤器链中抛出的任何AccessDeniedException
和AuthenticationException
。 - **
FilterSecurityInterceptor
**:负责权限校验的过滤器。
在我们的入门案例中,我们访问的路径被拦截需要授权才可以对后台进行访问,在UsernamePasswordAuthenticationFilter
过滤器中,会提取我们输入的用户名和密码并且封装成Authentication
对象,然后调用我们ProviderManager
进行授权,在这里我们的ProviderManager
其实也只是一个代理转发的功能,本身并不直接处理身份认证请求,它会委托给内部配置的Authentication Provider列表providers。该列表会进行循环遍历,依次对比匹配以查看它是否可以执行身份验证。这一步其实就是看看我们当前登录哪一个provider
可以进行授权,找到哪个可以授权的进行authticate
操作。
这个时候我们就是用的DaoAuthenticationProvider
来进行授权操作的,这个逻辑我会简单讲讲,后面会在源码分析细讲。我们这个类中调用了实现了UserDetailService
的对象的loadUserByUserName
方法,用来寻找对应用户名的用户信息。在这里我们默认的是inMemoryUserDetailManager
,这个用户就是我们生成在内存的那一个。我们根据用户名获取到了对象之后,将对象封装成UserDetail
返回我们的provider
,在这里面我们会进行密码的校验,如果用户密码校验通过我们会将UserDetail
封装成Authentication
对象返回UserNamePasswordAuthenticationFilter
来将我们的Authentication
加入到我们的SecurityContextHolder
中,因为这个对象是存在我们的LocalThread
中的,我们可以通过这个对象来获取当前用户的信息。
自定义授权(从数据库中获取对象)
因为我们入门案例过于简单,并且用户的查询操作过于简单,所以这部分总结一下自定义loadUserByName
方法,来进行授权。当然,我们跳过了登陆页面这一步,直接是放行登录页面进行用户的授权。需要注意:为什么校验通过了之后,将用户信息加入到SecurityContextHolder
就不用再进行校验这个逻辑,我还并没有搞清楚,先挖个坑!!。
从我们上一个部分的分析我们可以知道,我们只需要在登录逻辑中调用ProviderManager
来进行校验,并且将返回的Authntication
加入到我们Spring 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<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>数据库配置
1
2
3
4
5
6spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/springsecurity_learn?useUnicode=true&useSSL=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver创建
SecurityConfig
配置类,放行login
并且配置AuthenticationManager
和PasswordEncoder
。(因为我们DaoAuthenticationProvider
用到了密码的编码器来比对密码是否正确,需要注意要配置一个PasswordEncoder
对象并且在数据库中需要存储加密之后的密码)。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
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public PasswordEncoder getPassWordEncoder(){
return new BCryptPasswordEncoder();
}
// 新建一个AuthenticationManager对象
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
protected void configure(HttpSecurity http) throws Exception {
// 禁用crsf
http.csrf().disable()
// 配置请求 放行login 其余请求都需要授权
.authorizeRequests().antMatchers("/login").permitAll()
.anyRequest().authenticated();
}
}创建继承
UserDetails
类对象LoginUser
。需要注意里面重写的方法,判断用户是否过期,是否上锁什么的需要改成false
。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
public class LoginUser implements UserDetails , Serializable {
private static final long serialVersionUID = -3196224848843082710L;
Integer userId;
String username;
String password;
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
public String getPassword() {
return this.password;
}
public String getUsername() {
return this.username;
}
public boolean isAccountNonExpired() {
return true;
}
public boolean isAccountNonLocked() {
return true;
}
public boolean isCredentialsNonExpired() {
return true;
}
public boolean isEnabled() {
return true;
}
}创建用户对应
mapper
和service
1
2public interface MyMapper extends BaseMapper<LoginUser> {
}1
2
3
4
5
6
7
8
9
10
11
12
public class MyServiceImpl implements MyService {
MyMapper mapper;
public LoginUser findUserByName(String username) {
QueryWrapper<LoginUser> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
LoginUser loginUser = mapper.selectOne(wrapper);
return loginUser;
}
}创建实现
UserDetailsService
接口的类,实现loadUserByUsername
,完成查询用户逻辑。1
2
3
4
5
6
7
8
9
10
public class UserDetailServiceImpl implements UserDetailsService {
MyService service;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return service.findUserByName(username);
}
}完成用户登录对应
Controller
和Service
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class LoginServiceImpl implements LoginService {
AuthenticationManager manager;
public void login(String username, String password) {
//调用AuthenticationManager的 authenticate方法,进行用户认证。
Authentication authentication = new UsernamePasswordAuthenticationToken(username,password);
Authentication authenticate = null;
try {
authenticate = manager.authenticate(authentication);
}catch (Exception e){
e.printStackTrace();
}
//将认证后的信息加入上下文
SecurityContextHolder.getContext().setAuthentication(authenticate);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyController {
LoginService service;
public String login(String username,String password){
service.login(username,password);
return "ok";
}
public String getIndex(String username,String password){
service.login(username,password);
return "hello!";
}
}
效果
使用postman进行调试,没有登录的时候访问/hello
会被拒绝。
登陆之后访问:
这个时候后台会报如下错,这个是正常错误,因为我们没有获取到用户名和密码,然后调用UserDetailsService
获取会出错。
1 | org.springframework.security.authentication.InternalAuthenticationServiceException: UserDetailsService returned null, which is an interface contract violation |
自定义授权(jwt授权)
如果对于jwt不够熟悉可以去这里
两种认证方式
1) 基于Session的认证方式
session 认证流程:
- 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
- 请求返回时将此 Session 的唯一标识 SessionID 返回给浏览器
- 浏览器接收到服务器返回的 SessionID 后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名
- 当用户第二次访问服务器的时候,请求会自动把此域名下的 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。
根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。
session 认证存在的问题
- 在分布式的环境下,基于session的认证会出现一个问题,每个应用服务都需要在session中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用服务需要将session信息带过去,否则会重新认证。我们可以使用Session共享、Session黏贴等方案。
2) 基于Token的认证方式
什么是Token? (令牌)
- 访问资源接口(API)时所需要的资源凭证
- 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)
服务器对 Token 的存储方式:
- 存到数据库中,每次客户端请求的时候取出来验证(服务端有状态)
- 存到 redis 中,设置过期时间,每次客户端请求的时候取出来验证(服务端有状态)
- 不存,每次客户端请求的时候根据之前的生成方法再生成一次来验证(JWT,服务端无状态)
Token特点:
- 服务端无状态化、可扩展性好
- 支持移动端设备
- 安全
- 支持跨程序调用
token 的身份验证流程:
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端
- 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 token
- 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据
每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里
注意:
登录时 token 不宜保存在 localStorage,被 XSS 攻击时容易泄露。所以比较好的方式是把 token 写在 cookie 里。为了保证 xss 攻击时 cookie 不被获取,还要设置 cookie 的 http-only。这样,我们就能确保 js 读取不到 cookie 的信息了。再加上 https,能让我们的请求更安全一些。
token认证方式的优缺点
- 优点: 基于token的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把token 存在任意地方,并且可以实现web和app统一认证机制。
- 缺点: token由于自包含信息,因此一般数据量较大,而且每次请求都需要传递,因此比较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理负担。
3) Token 和 Session 的区别
- Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。
- Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重复攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。
代码
新增导入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<!--Spring Data Redis为我们封装了Redis客户端的各种操作,简化使用-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis工具类使用fastjson【序列化】 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.28</version>
</dependency>
<!-- jwt工具类用到的依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>更改配置文件
application.yml
,配置redis信息。1
2
3
4
5
6
7
8
9spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/springsecurity_learn?useUnicode=true&useSSL=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: 127.0.0.1
port: 6379新增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/**
* Redis使用FastJson进行序列化
* @author spikeCong
* @date 2023/4/10
**/
public class FastJsonJsonRedisSerializer<T> implements RedisSerializer<T> {
private ObjectMapper objectMapper = new ObjectMapper();
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static
{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
public FastJsonJsonRedisSerializer(Class<T> clazz)
{
super();
this.clazz = clazz;
}
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
public T deserialize(byte[] bytes) throws SerializationException
{
if (bytes == null || bytes.length <= 0)
{
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
public void setObjectMapper(ObjectMapper objectMapper)
{
Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper = objectMapper;
}
protected JavaType getJavaType(Class<?> clazz)
{
return TypeFactory.defaultInstance().constructType(clazz);
}
}新增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
public class RedisConfig {
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
//配置连接工厂
template.setConnectionFactory(connectionFactory);
//使用FastJson2JsonRedisSerializer 来序列化和反序列化redis的value值
FastJsonJsonRedisSerializer serializer = new FastJsonJsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
//指定要序列化的域: field,get和set,以及修饰符范围,ANY表示包括private和public
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//指定序列化输入的类型,类必须是非final修饰的, final修饰的类会报异常.
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
//redis中存储的value值,采用json序列化
template.setValueSerializer(serializer);
//redis中的key值,使用StringRedisSerializer来序列化和反序列化
template.setKeySerializer(new StringRedisSerializer());
//初始化RedisTemplate的一些参数设置
template.afterPropertiesSet();
return template;
}
}新增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
206
207
208
209
210
211
212
213/**
* spring redis 工具类
*/
public class RedisCache
{
public 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> long setCacheSet(final String key, final Set<T> dataSet)
{
Long count = redisTemplate.opsForSet().add(key, dataSet);
return count == null ? 0 : count;
}
/**
* 获得缓存的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 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);
}
}新增
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
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/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "zhanggeyang";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
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("sg") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}
public static void main(String[] args) throws Exception {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
Claims claims = parseJWT(token);
System.out.println(claims);
}
/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}新建统一返回类
BaseResponse
并且修改登录逻辑,我们登录之后将用户存在redis中,并返回token。authentication
添加到securityContextHolder
中变成第一次携带token访问的时候再加上。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BaseResponse<T> implements Serializable {
private static final long serialVersionUID = 6492046450268487443L;
private String code;
private String message;
private T data;
private BaseResponse<T> success(T data){
return new BaseResponse("200","操作成功",data);
}
private BaseResponse<T> error(String message){
return new BaseResponse("500",message,null);
}
}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
public class LoginServiceImpl implements LoginService {
AuthenticationManager manager;
RedisCache redisCache;
public String login(String username, String password) {
//调用AuthenticationManager的 authenticate方法,进行用户认证。
Authentication authentication = new UsernamePasswordAuthenticationToken(username,password);
Authentication authenticate = null;
try {
authenticate = manager.authenticate(authentication);
}catch (Exception e){
e.printStackTrace();
}
//获取经过验证的身份信息
LoginUser principal = (LoginUser) authenticate.getPrincipal();
//根据userid生成对应jwt
String token = JwtUtil.createJWT(principal.getUserId().toString());
//将用户信息存入redis,下一次请求可以识别
redisCache.setCacheObject("userId:"+authenticate.getPrincipal().toString(),principal);
//返回token
return token;
// //将认证后的信息加入上下文
// SecurityContextHolder.getContext().setAuthentication(authenticate);
}
}添加过滤器,每次我们携带token访问的时候需要将我们认证的信息添加到
SecurityContextHolder
。需要注意我们这个过滤器需要添加到UsernamePasswordAuthenticationFilter
之前,原因是这个过滤器之前一个过滤器就是让我们能够使用SecurityContextHolder
。所以为什么必须在UsernamePasswordAuthenticationFilter
之前。这个不知道,挖一个坑!!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
public class SecurityFilter implements Filter {
RedisCache redisCache;
public void doFilter(ServletRequest request, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request1 = (HttpServletRequest) request;
String token = request1.getHeader("token");
if(!StringUtils.hasText(token))
{
//放行,之后授权会失败的
filterChain.doFilter(request,servletResponse);
//防止回来了继续执行
return;
}
String userId = null;
try {
userId = JwtUtil.parseJWT(token).getSubject();
} catch (Exception e) {
throw new RuntimeException(e);
}
//获取redis中的消息
LoginUser user = redisCache.getCacheObject("userId:" + userId);
if(Objects.isNull(user)){
throw new RuntimeException("用户没有登录");
}
//将用户新保存到SecurityContextHolder,以便后续的访问控制和授权操作使用。
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request,servletResponse);
}
}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
public class SecurityConfig extends WebSecurityConfigurerAdapter {
Filter securityFilter;
public PasswordEncoder getPassWordEncoder(){
return new BCryptPasswordEncoder();
}
// 新建一个AuthenticationManager对象
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
protected void configure(HttpSecurity http) throws Exception {
// 禁用crsf
http.csrf().disable()
// 配置请求 放行login 其余请求都需要授权
.authorizeRequests().antMatchers("/login").permitAll()
.anyRequest().authenticated();
//指定过滤器的顺序
http.addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class);
}
}