RSA:非对称加密,有公钥和私钥之分,公钥用于数据加密,私钥用于数据解密。加密结果可逆,公钥一般提供给外部进行使用,私钥需要放置在服务器端保证安全性。特点:加密安全性很高,但是加密速度较慢
AES: Advanced Encryption Standard 高级加密标准,最常见的对称加密算法,即加密和解密使用同样的密钥,加密结果可逆,特点:加密速度非常快,适合经常发送数据的场合。
MD5加密:单向加密算法特点:加密速度快,不需要秘钥,但是安全性不高,需要搭配随机盐值使用。
1// controller通知增强,针对controller设定触发事件与处理方法
2// @ExceptionHandler:进行全局异常处理
3// @InitBinder:绑定前台请求参数到Model中,全局数据预处理
4// @ModelAttribute:绑定键值对到Model中,全局数据绑定
5// https://juejin.cn/post/6844904168025489421
6
7// 全局异常处理,所以拥有高优先级
8Ordered.HIGHEST_PRECEDENCE) (
9public class CommonGlobalExceptionHandler {
10 // 拦截异常的种类,拦截全局异常时使用Exception.class
11 value = Exception.class) (
12 // 拦截异常后可以选择返回的json对象
13
14 public JsonResponse<String> commonExceptionHandler(HttpServletRequest request,Exception exception){
15 String msg=exception.getMessage();
16 // 主动抛出异常的处理,如密码错误抛出异常
17 if(exception instanceof ConditionException){
18 String code=((ConditionException)exception).getCode();
19 return new JsonResponse<>(code,msg);
20 }
21 // 其它未知异常
22 return new JsonResponse<>("500",msg);
23 }
24}
HTTP 无状态协议,需要额外的配置实现状态保存。
是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上,用于告知服务端前后两个请求是否来自同一浏览器。本地保存别人可以分析存放在本地的COOKIE并进行COOKIE欺骗。
基于 cookie 实现的一种认证方式,主要作用就是通过服务端记录用户的状态。服务器端接受客户端请求后,建立一个session,并发送一个http响应到客户端,在响应中包含sessionId,客户以cookie的方式保存sessionId,在客户端发起的第二次请求时,浏览器会自动在请求头中带上cookie,服务器根据cookie找到session恢复数据环境。随着用户的增多,服务端压力增大。分布式下拓展性不强,粘性会话 Sticky Session:尽量让同一个用户的请求落到一台机器上。缺点:如果当前机器下线则用户的信息全部丢失。会话复制 Session Replication:将会话信息复制到所有机器上,无论用户请求落到哪台机器上都能取到之前的会话信息。缺点:复制需要成本,冗余过大,难以保证所有机器上会话信息一致。集中会话 Centralized Session:JDBC、Redis等集中保存信息,机器需要信息时到JDBC,Redis中取。
CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
受害者登录a.com,并保留了登录凭证(Cookie)->攻击者引诱受害者访问了b.com->b.com 向 a.com 发送了一个请求:a.com/act=xx->浏览器会默认携带a.com的Cookie->a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求->a.com以受害者的名义执行了act=xx->攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。
解决方法:1,同源检测:CSRF大多来自第三方网站,那么就直接禁止外域(或者不受信任的域名)对我们发起请求。2,CSRF Token:攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用Cookie中的信息。要求所有的用户请求都携带一个CSRF攻击者无法获取到的Token。服务器通过校验请求是否携带正确的Token,来把正常的请求和攻击的请求区分开,也可以防范CSRF的攻击。
类似于无状态的临时的证书签名,由uid+time+sign[+固定参数]组成,服务端验证浏览器携带的用户名和密码,验证通过后生成用户令牌(token)并返回给浏览器,浏览器再次访问时携带token,服务端校验token并返回相关数据。服务端不用存放 token 数据,用解析 token 的计算时间换取 session 的存储空间,减轻服务器存储压力。安全性高,分布式系统下扩展性强。
全称是JSON Web Token,用于在空间受限环境下安全传递“声明”,JWT跨语言支持、便于传输、易于扩展。JWT分成三部分,头部(header:声明的类型、声明的加密算法),第二部分是载荷(payload:存放有效信息,一般包含签发者、所面向的用户、接受方、过期时间、签发时间以及唯一身份标识,防篡改,不防泄露),第三部分是签名(signature:主要由头部、载荷以及秘钥组合加密而成)。
缺点:用户状态变化(删除,禁用,注销等)影响到业务而Token仍然有效时,仍然能利用token完成认证,当token过期后需要用户重新登录,用户体验差。
解决办法使用 accessToken (负责后端业务验证)+ refreshToken(负责续签验证)。认证后返回 accessToken + refreshToken,并保存在本地,服务端保存refreshToken,accessToken 失效时间应该设置较短,比如10分钟,refreshToken 失效时间可以长一点,比如 7 天。请求时只用 accessToken,客户端在 accessToken 在失效前主动发起请求用 refreshToken 返回一个新的 accessToken,或者在正常业务请求时判断access-token是否过期,过期了就顺带更新,用户无感提高用户体验。退出时客户端删除accessToken + refreshToken,服务端删除refresh-token。
x1// 加密生成access-token,使用userId标识用户
2public static String generateToken(Long userId) throws Exception{
3 Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(), RSAUtil.getPrivateKey());
4 Calendar calendar = Calendar.getInstance();
5 // 当前时间
6 calendar.setTime(new Date());
7 // token过期时间1小时
8 calendar.add(Calendar.HOUR, 1);
9 return JWT.create().withKeyId(String.valueOf(userId))// 唯一身份标识
10 .withIssuer(ISSUER)// token签发者
11 .withExpiresAt(calendar.getTime())// token过期时间
12 .sign(algorithm);
13}
14
15// 加密生成refresh-token,使用userId标识用户
16public static String generateRefreshToken(Long userId) throws Exception{
17 Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(), RSAUtil.getPrivateKey());
18 // 当前时间
19 Calendar calendar = Calendar.getInstance();
20 calendar.setTime(new Date());
21 // token过期时间7天
22 calendar.add(Calendar.DAY_OF_MONTH, 7);
23 return JWT.create().withKeyId(String.valueOf(userId))
24 .withIssuer(ISSUER)
25 .withExpiresAt(calendar.getTime())
26 .sign(algorithm);
27}
28// 养成token
29public static Long verifyToken(String token){
30 try{
31 // 解密出uid
32 Algorithm algorithm = Algorithm.RSA256(RSAUtil.getPublicKey(), RSAUtil.getPrivateKey());
33 JWTVerifier verifier = JWT.require(algorithm).build();
34 DecodedJWT jwt = verifier.verify(token);
35 String userId = jwt.getKeyId();
36 return Long.valueOf(userId);
37 }catch (TokenExpiredException e){
38 // token过期
39 throw new ConditionException("555","token过期!");
40 }catch (Exception e){
41 // 解密失败
42 throw new ConditionException("非法用户token!");
43 }
44}
用户表t_user
、用户信息表t_user_info
。t_user
在登陆时手机号经常被查询所以对phone
建立索引。t_user_info
使用useId
作为外键,建立与表t_user
的联系,根据用户ID获取用户信息时userId
经常被查询,所以添加索引。t_refresh_token
用于保存用户的refresh_token
。
xxxxxxxxxx
481-- ----------------------------
2-- Table structure for t_user
3-- ----------------------------
4DROP TABLE IF EXISTS `t_user`;
5CREATE TABLE `t_user` (
6 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
7 `phone` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '手机号',
8 `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '邮箱',
9 `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '密码',
10 `salt` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '盐值',
11 `createTime` datetime DEFAULT NULL COMMENT '创建时间',
12 `updateTime` datetime DEFAULT NULL COMMENT '更新时间',
13 PRIMARY KEY (`id`),
14 INDEX phone_index ( `phone`)
15) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
16
17-- ----------------------------
18-- Table structure for t_user_info
19-- ----------------------------
20DROP TABLE IF EXISTS `t_user_info`;
21CREATE TABLE `t_user_info` (
22 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
23 `userId` bigint DEFAULT NULL COMMENT '用户id',
24 `nick` varchar(100) DEFAULT NULL COMMENT '昵称',
25 `avatar` varchar(255) DEFAULT NULL COMMENT '头像',
26 `sign` text COMMENT '签名',
27 `gender` varchar(2) DEFAULT NULL COMMENT '性别:0男 1女 2未知',
28 `birth` varchar(20) DEFAULT NULL COMMENT '生日',
29 `createTime` datetime DEFAULT NULL COMMENT '创建时间',
30 `updateTime` datetime DEFAULT NULL COMMENT '更新时间',
31 PRIMARY KEY (`id`),
32 INDEX userId_index ( `userId`)
33) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户基本信息表';
34alter table `t_user_info` add constraint FK_user_info_userId foreign key (`userId`) references `t_user`(`id`);
35
36-- ----------------------------
37-- Table structure for t_refresh_token
38-- ----------------------------
39DROP TABLE IF EXISTS `t_refresh_token`;
40CREATE TABLE `t_refresh_token` (
41 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
42 `userId` bigint DEFAULT NULL COMMENT '用户id',
43 `refreshToken` varchar(500) DEFAULT NULL COMMENT '刷新令牌',
44 `createTime` datetime DEFAULT NULL COMMENT '创建时间',
45 PRIMARY KEY (`id`),
46 INDEX userId_index ( `userId`)
47) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='刷新令牌记录表';
48alter table `t_refresh_token` add constraint FK_refresh_token_userId foreign key (`userId`) references `t_user`(`id`);
双方使用RSA对密码加解密,以注册时间为salt,salt与原始密码使用MD5算法构建签名,构建User
存入user
表。同时构建UserInfo
存入user_info
表。
xxxxxxxxxx
131// 生成盐
2Date now = new Date();
3String salt = String.valueOf(now.getTime());
4// 解密获得原始密码
5String password = user.getPassword();
6String rawPassword;
7try {
8 rawPassword = RSAUtil.decrypt(password);
9} catch (Exception e) {
10 throw new ConditionException("密码解密失败!");
11}
12// 解密后原始密码+盐生成签名
13String md5Password = MD5Util.sign(rawPassword, salt, "UTF-8");
将收到密码解密后,与数据库中相同手机号的用户的salt
构建签名,再将签名与数据库中签名比较以完成认证。成功后根据用户ID生成accessToken,refreshToken
返回给用户用于后续请求认证,同时将refreshToken
保存在数据库中用于后续刷新accessToken
。
xxxxxxxxxx
281// 手机号对应用户是否存在
2User dbUser = userDao.getUserByPhone(phone);
3// 对接收到user密码解码
4String password = user.getPassword();
5String rawPassword;
6try {
7 rawPassword = RSAUtil.decrypt(password);
8} catch (Exception e) {
9 throw new ConditionException("密码解密失败!");
10}
11// 将解密后密码与盐构建签名
12String salt = dbUser.getSalt();
13String md5Password = MD5Util.sign(rawPassword, salt, "UTF-8");
14// 将签名与数据库中用户签名比较
15if (!md5Password.equals(dbUser.getPassword())) {
16 throw new ConditionException("密码错误!");
17}
18// 生成access-token、refresh-token
19Long userId = dbUser.getId();
20String accessToken = TokenUtil.generateToken(userId);
21String refreshToken = TokenUtil.generateRefreshToken(userId);
22//保存refresh token到数据库
23userDao.deleteRefreshToken(refreshToken, userId);
24userDao.addRefreshToken(refreshToken, userId, new Date());
25Map<String, Object> result = new HashMap<>();
26result.put("accessToken", accessToken);
27result.put("refreshToken", refreshToken);
28return result;
登录成功后获得token
xxxxxxxxxx
51{
2"accessToken":"eyJraWQiOiIyNiIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJpc3MiOiLnrb7lj5HogIUiLCJleHAiOjE2NTA2MjU2MDV9.j25e0JlPPj9-x4seDVBNH9eDBUwrH2mBoKR7zCpiWLAcWUmYU4S8DMuOh1bcp1zzES10_Hpc5VjlDTeHRvG-hVpJ5HN5dNF3P84cGnpIl3WH4oepDz-OEviyW0I0bOWsYRWUCbPnih9j7c4vTdA3ZUvWMqRdrHBbwpdKkopxMWI",
3
4"refreshToken":"eyJraWQiOiIyNiIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJpc3MiOiLnrb7lj5HogIUiLCJleHAiOjE2NTEyMjY4MDV9.BxXnoGCrD6Y0DXzSIfFX2v9HQyRC71Sz1poISlqeb9YUPhmbeWh-vRe2n9s_iU1AIfiDQrS3HfQAi6MQMPa1VUcQkuuHRqDytCkeK_hDdQ_T7HhJzqHU_-SlHTqcr6v2Z6edAU4_c8nHkv0lGRe4esU_FzD-gzLlGYw_p7c_mhk"
5}
登陆后信息获取依赖accessToken
完成认证,在请求头中添加token信息,服务器获取从请求头获得token后解密获得用户ID,完成认证,避免再次登录。最后根据用户ID从数据库中找到并返回用户信息。
xxxxxxxxxx
121// 根据请求头中token解码出userId
2public Long getCurrentUserId(){
3 // 抓取请求上下文
4 ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
5 String token = requestAttributes.getRequest().getHeader("token");// 获取请求头中的token
6 // 解密获得userId
7 Long userId = TokenUtil.verifyToken(token);
8 if(userId < 0){
9 throw new ConditionException("非法用户!");
10 }
11 return userId;
12}
服务器获取从请求头获得accessToken
后解密获得用户ID,完成认证。更新时先将新密码和salt
构建新签名,再更新其它信息。注意:用户上传信息可能只包含被更改信息,未更改信息字段为空,在更新时需要判空, mybatis下判断user
下各个字段,非空下才更新,防止数据丢失。UserInfo
的更新同理。
xxxxxxxxxx
91if (!StringUtils.isNullOrEmpty(user.getPassword())) {
2 String rawPassword = RSAUtil.decrypt(user.getPassword());
3 String md5Password = MD5Util.sign(rawPassword, dbUser.getSalt(), "UTF-8");
4 user.setPassword(md5Password);
5}
6// 设置更新时间
7user.setUpdateTime(new Date());
8// mybatis下判断user下各个字段,非空下才更新,防止数据丢失
9userDao.updateUsers(user);
xxxxxxxxxx
181<update id="updateUsers" parameterType="com.imooc.bilibili.domain.User">
2 update
3 t_user
4 set
5 <--判空处理,防止信息被替换为空白字段-->
6 <if test="phone != null and phone != '' ">
7 phone = #{phone},
8 </if>
9 <if test="email != null and email != '' ">
10 email = #{email},
11 </if>
12 <if test="password != null and password != '' ">
13 password = #{password},
14 </if>
15 updateTime = #{updateTime}
16 where
17 id = #{id}
18</update>
客户端在 accessToken 在失效前主动发起请求用 refreshToken
返回一个新的 accessToken
,或者在正常业务请求时判断accessToken
是否过期,过期了就顺带更新,用户无感提高用户体验。
1// 当access-token过期后,依据用户的refresh-token重新生成access-token
2public String refreshAccessToken(String refreshToken) throws Exception {
3 // 验证refresh-token是否过期
4 RefreshTokenDetail refreshTokenDetail = userDao.getRefreshTokenDetail(refreshToken);
5 if (refreshTokenDetail == null) {
6 throw new ConditionException("555", "token过期!");
7 }
8 // 依据用户的refresh-token重新生成access-token
9 Long userId = refreshTokenDetail.getUserId();
10 return TokenUtil.generateToken(userId);
11}
更新acciss0-token后客户端收到新的access-token
xxxxxxxxxx
11 "data":"eyJraWQiOiIyNiIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJpc3MiOiLnrb7lj5HogIUiLCJleHAiOjE2NTA2MjU5NTR9.T-26sV-LAv-OoNnziq5D5VseioDW8OxUwdE9qjPASdcW9-9zRCg9r7PY0WLMwYS4Si2Lt9xCzPweL8h5JOb4TjtwMc952-yL-a5sJ2h2gyzb5UlXIJ941iT_iN8uJZIcgrBM7Q8L0dvdT5Gdj6T0G0Nku3eeeuPrJf1bCD9TGws",