认证-鉴权

一般的web项目当中,总会有登陆和鉴权的需求

  • 认证:验证当前访问的用户是不是本系统中的用户。确定是哪一个具体的用户。
  • 鉴权:经过认证,判断当前登陆用户有没有权限来执行某个操作。

所以说,安全框架SpringSecurity当中,必定会有认证和鉴权的两大核心功能。

入门demo

(1)项目中引入SpringSecurity

1
2
3
4
5
<!--引入security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

(2)引入相关依赖后再次登录localhost:8080 会发现需要先进行登录才能访问(springsecurity内置的):

可以使用console输出的md5值进行登录:Using generated security password: dab4b80e-9385-4a0c-8140-d1dcdb83e53e(用户名默认为user)

认证

一般web登录流程如下图所示:

img

SpringSecurity现有缺点:

  1. 用户使用的是security给的默认用户名和密码,想真实地去数据库里(tb_user)获取真实的用户名和密码
  2. security自带的cookie\session模式,希望使用jwt进行无状态登陆
  3. 前端页面应该携带jwt(请求头里带上)然后发送至后端
  4. 鉴权操作完全没有,想鉴权做完善

源码

SpringSecurity实际上就是通过一些过滤器、拦截器来实现登录鉴权的流程的:

(1)springsecurity 登陆流程

springsecurity就是一个过滤器链,内置了关于springsecurity的16个过滤器:

  • UsernamePasswordAuthenticationFilter:处理我们登陆页面输入的用户名和密码是否正确的过滤器
  • ExceptionTranslationFilter:处理前面的几个过滤器中,有了问题,抛出错误,不让用户登录
  • FilterSecurityInterceptor:经行一个权限校验的拦截器

上述只标出了几个核心过滤器,我们可以找到当前boot项目中所有有关security的过滤器链

UsernamePasswordAuthenticationFilter 运行机制

自定义登录流程

思路如下:

(1)登陆:自定义登录接口、 调用providermanager的authenticate()方法、 登陆成功后还需生成jwt,最后将jwt存入redis;

自定义UserDetailsManager实现类、 从数据库中获取系统用户

(2)访问资源:自定义认证过滤器、获取token、从token中获取userid、从redis中通过userid获取用户信息、如果该user为第一次访问还需存入SecurityContextHolder,这样其他过滤器就可以从SecurityContextHolder获取到所有信息

login-process

无状态

(1)有状态登录(Session):
传统上,我们会使用 Session 和 Cookie 来保存用户的授权信息。第一步,登录过程,用户使用用户名和密码来登录系统,服务器会来验证用户名和密码是否正确,如果正确,服务器会给这个用户创建一个包含用户登录信息、角色、权限的一个叫做 Session 的东西,然后把这个 Session 保存起来,同时把这个 Session 的 ID 以 Cookie 的形式发送给前端,表示用户验证成功,登录完成了;接下来如果用户希望访问某些资源,前端要向后端发起一个 HTTP 的请求,同时相应的 Cookie 也会跟随请求一起发送给服务器,而服务器取得 Cookie 以后就会去查找是否有 Session ID ,然后通过 Session ID 提取相应的 Session 来确定用户的身份与权限,如果 Session 与 ID 相符,同时用户的信息也能提供相应的权限,服务器就会认为这个用户已经登录了,随后资源信息就会通过 HTTP 响应给前端
(2)无状态登陆(JWT):
第一步,同样是登陆,用户使用用户名和密码登陆,如果登陆成功,服务器就会返回一个加密文档,这个文档就是 JWT ,其中包含用户密码以外,全部的认证信息,包括用户名、Email、角色、权限等等,而前端在拿到 这个JWT 以后就可以把它保存起来了,可以保存到 Cookie 中,也可以保存到浏览器的 LocaStorage 里面,而生成的 JWT 不需要在后端保存,接下来第二步,用户如果需要访问某些权限的时候,这时候,用户就要把 JWT 放在 HTTP 请求 herder 中与请求一起发送给服务器,服务器取得 JWT 以后 会使用私钥给 JWT 文档解密 ,如果解密成功而且数据依然有效则代表用户已经登陆了,如果 JWT 所描述的用户权限允许该用户访问资源,那么服务器就会把资源的信息,通过 HTTP 响应发回给前端

区别与差异: 传统上用户登陆状态会以 Session 的形式保存在服务器上,而 Session ID 则保存在前端的 Cookie 中;而使用 JWT 以后,用户的认证信息将会以 Token 的形式保存在前端,服务器不需要保存任何的用户状态,这也就是为什么 JWT 被称为无状态登陆的原因,无状态登陆最大的优势就是完美支持分布式部署,可以使用一个 Token 发送给不同的服务器,而所有的服务器都会返回同样的结果

JWT

JWT 全称: Json Web Token
  作用: JWT 的作用是 用户授权(Authorization) ,而不是用户的身份认证(Authentication) 。
  用户认证 指的是使用用户名、密码来验证当前用户的身份,即用户登录。
  用户授权 指用户登录成功后,当前用户有足够的权限访问特定的资源。

JSON Web Token(JWT)是一个非常轻巧的规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息,且无状态,不需要服务器端存session;虽然能被看到但是不能被篡改,因为第三部分使用密钥进行了加密

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名

1. 头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象,下面字符串在头部指明了签名算法是HS256算法

1
{"typ":"JWT","alg":"HS256"}

2. 载荷(payload)

载荷就是存放有效信息的地方。

定义一个payload:

1
{"sub":"1234567890","name":"itlils","admin":true,"age":18}

3.签证(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的编码)

payload (base64后的编码)

secret

这个部分需要base64加密后的header和base64加密后的payload双方使用.连接组成字符串,然后通过header中声明的加密方式(对称加密HMAC SHA256/非对称加密RS256)进行加盐secret组合加密,然后就构成了jwt的第三部分

测试token

首先引入依赖:

1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

加密解密过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//加密过程
JwtBuilder jwtBuilder= Jwts.builder()
.setId("5555") //设置ID
.setSubject("TestJwt") //设置主题
.setIssuedAt(new Date()) //签发日期
.setExpiration(date+5000) //设置该token5秒后过期
//设置加密算法和密钥(对称、非对称)
.signWith(SignatureAlgorithm.HS256,"emei268");
String jwt = jwtBuilder.compact();
System.out.println(jwt);

//解密过程
Claims emei268 = Jwts.parser().setSigningKey("emei268").parseClaimsJws(jwt).getBody();
System.out.println(emei268);

输出结果如下所示:

1
2
3
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI1NTU1Iiwic3ViIjoiVGVzdEp3dCIsImlhdCI6MTY3MzUzMzgwNH0.ONgwjGRqt2PIltPrQRGzwqGHMPCuBYBKOOaRTychnsU

{jti=5555, sub=TestJwt, iat=1673533804}

准备新项目

首先新建两个配置类(针对redis序列化问题)

  1. RedisCache.java
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
214
215
216
217
218
219
220
221
222
223
224
225
226
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Autowired
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> 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);
}
}
  1. FastJsonRedisSerializer.java
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
/**
* 对Redis使用FastJson序列化
*
* @author itlils
*/
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{

public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

private Class<T> clazz;

static
{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}

public FastJsonRedisSerializer(Class<T> clazz)
{
super();
this.clazz = clazz;
}

@Override
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}

@Override
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);
}


protected JavaType getJavaType(Class<?> clazz)
{
return TypeFactory.defaultInstance().constructType(clazz);
}
}

然后新建两个工具类:

  1. JwtUtil.java
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
/**
* JWT工具类
*/
public class JwtUtil {

//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "itlils";

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("ydlclass") // 签发者
.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();
}


/**
* 生成加密后的秘钥 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();
}
}
  1. WebUtils.java
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;
}
}

最后创建User实体类以及ResponseResult类(这里就不再赘述)