密钥交互和用户认证

Snipaste_2024-08-28_10-26-02

1.前端获取SM2公钥

image-20231230131522212

  • 通过钩子函数当用户访问登陆页面时请求获取公钥,并存入sessionStorage

  • 前端axios请求

    src\views\Login.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mounted() {
this.axios.get('/getPublicKey').then((resp) =>{
let data = resp.data;
if(data.code==200){
sessionStorage.setItem('publicKey', data.data.publicKey);
} else{
this.$message({
message: "获取公钥失败",
type:'error'
});
}
}).catch(error => {
// 处理错误...
});
}
  • 后端从配置文件中获取到SM2publicKey并返回前端

    com/gaomu/controller/LoginController.java

1
2
3
4
@RequestMapping("/getPublicKey")
public ResponseResult getPublicKey(){
return secretKeyService.getPublicKey();
}
  `com/gaomu/server/impl/SecretKeyServiceImpl.java`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class SecretKeyServiceImpl implements SecretKeyService {

@Value("${secretKey.publicKey}")
private String publicKey;

@Override
public ResponseResult getPublicKey() {
System.out.println("publicKey :" + publicKey );
Map<String, String > map = new HashMap<>();
map.put("publicKey", publicKey);
return new ResponseResult(200, "获取成功", map);
}
}
  • 配置文件中的公钥

    src/main/resources/application.yml

1
2
secretKey:
publicKey: 04ac65e37746267acd937a6532be574cf48e678f33f535b045baa92c3533892f8d66bff57cad3fe8e37a61a2693ca49c21c933c57b211e04bf104dc4a5d46aa3dc

2.前端生成SM4密钥

  • 生成sm4密钥**(16进制密钥)**并存入sessionStorage

  • sm4需要128bits的密钥,因此需要16进制密钥字符位数为(128/4=32)(一位十六进制可以表示四位二进制)

    src\views\Login.vue

1
2
3
SM4Data = generateSM4KeyPair(SM4Data)
sessionStorage.setItem('secretKey', SM4Data.key)
sessionStorage.setItem('iv', SM4Data.iv)
  • 我这里使用的是sm2密钥生成器,可以生成64位16进制也就是256bits的长度的sm2私钥,我将其分成两份,一份128bits作为sm4的secretKey密钥,另一方128bits作为sm4的iv偏移量

    (注:任意128bits的密钥都可以作为sm4的密钥,因此我这里使用sm2密钥生成器,其实是为了生成sm4密钥而非sm2私钥,希望不要被误导)

    src\utils\encrypto\sm4.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let SM4Data = {
key: '',
iv: '',
originalData: '',
encryptedData: '',
decryptedData: ''
}

function generateSM4KeyPair (SM4Data) {
let SM2Pair = SM2.generateKeyPair()
SM4Data.key = SM2Pair.privateKey.substring(0, 32)
SM4Data.iv = SM2Pair.privateKey.substring(32, 64)
return SM4Data
}

3.前端用户认证

  • 将用户在页面中输入的用户名和密码存入表单
  • 添加一个tempPassword为了暂存用户输入的密码,如果直接双向绑定表单中的password,当password加密后页面中的密码字符会变得特别长,不友好

src\views\Login.vue

1
2
3
4
5
6
<el-form-item label="">
<el-input type="text" v-model="loginForm.userName" autocomplete="off" placeholder="账号"></el-input>
</el-form-item>
<el-form-item label="">
<el-input type="password" v-model="tempPassword" autocomplete="off" placeholder="密码"></el-input>
</el-form-item>
1
2
3
4
5
6
7
8
9
10
11
data() {
return {
loginForm: {
userName: '',
password: '',
secretKey: '',
iv: ''
},
tempPassword: ''
}
},

4.前端使用SM2公钥加密

  • 前端使用SM2公钥加密(password、key、iv)

src\views\Login.vue

1
2
3
this.loginForm.secretKey = encryptSM2(SM4Data.key, publicKey)
this.loginForm.iv = encryptSM2(SM4Data.iv, publicKey)
this.loginForm.password = encryptSM2(this.loginForm.password, publicKey)
  • 发送登陆请求包

    src\views\Login.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
this.axios.post('/user/login', this.loginForm).then((resp) =>{
let data = resp.data;
if(data.code==200){
this.loginForm = data.data;
sessionStorage.setItem('token', data.data.token)
console.log(data.data)
this.$message({
message: '登陆成功',
type:'success'
});
this.$router.push({path:'/Home'})
} else{
//若认证失败删除session中secretKey和iv,也可以直接当请求成功时才存密钥和iv
sessionStorage.removeItem('secretKey')
sessionStorage.removeItem('iv')
this.$message({
message: '登陆失败',
type:'error'
});
}
}).catch(error => {
// 处理错误...
});

image-20231230131624027

5.后端使用SM2私钥解密

  • 首先将User对象传入服务层

    com/gaomu/controller/LoginController.java

1
2
3
4
5
6
7
8
9
@Autowired
private LoginService loginService;

@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user){
System.out.println(user);
//登录实现
return loginService.login(user);
}
  • 实现UserDetailsService从数据库中获取用户完整信息

    com/gaomu/server/impl/UserDetailsServiceImpl.java

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

@Autowired
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

//查询用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<User>();
queryWrapper.eq(User::getUserName, username);
User user = userMapper.selectOne(queryWrapper);
//如果没有查询到用户就行抛出异常
if(Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误!");
}
//查询对应的权限信息
//把数据封装成UserDetails返回
return new LoginUser(user);
}
}

  • 后端从配置文件中获取到SM2私钥,解密前端传入的登陆请求数据,并传入user对象

    com/gaomu/server/impl/LoginServiceImpl.java

1
2
@Value("${secretKey.privateKey}")
private String privateKey;
1
2
3
user.setPassword(SM2Util.sm2Decrypt(user.getPassword(), privateKey));
user.setSecretKey(SM2Util.sm2Decrypt(user.getSecretKey(), privateKey));
user.setIv(SM2Util.sm2Decrypt(user.getIv(), privateKey));

6.将key、iv存入loginUser并存入redis

  • 用户认证

    com/gaomu/server/impl/LoginServiceImpl.java

1
2
3
4
5
6
7
//Authentication authenticate进行用户认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果认证没通过,给出对应提示
if(Objects.isNull(authenticate)){
throw new RuntimeException("authenticate 为空,登陆失败!");
}
  • 认证通过将密钥和iv存入loginUser,将用户id存入jwt

    com/gaomu/server/impl/LoginServiceImpl.java

1
2
3
4
5
6
//认证通过,使用userid生成jwt,jwt存入ResponseResult 返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
loginUser.getUser().setSecretKey(user.getSecretKey());
loginUser.getUser().setIv(user.getIv());
String loginUserKey = JwtUtil.getUUID();
String userid = loginUser.getUser().getId().toString();

7.生成JWT

image-20231230131920873

  • 使用JWT工具类生成jwt token,并将token封装返回前端😶

com/gaomu/server/impl/LoginServiceImpl.java

1
2
3
4
5
6
7
//切勿使用userid作为redis的key,这样每个用户只能有一个会话,会导致当一个用户同时两处登陆时,sm4密钥会被覆盖,而如果使用随机值作为redis的key每个会话一个键值对,这样就不会导致密钥被覆盖的的问题
String jwt = JwtUtil.createJWT(loginUserKey, userid, null);
Map<String, String > map = new HashMap<>();
map.put("token", jwt);
//把完整的用户信息存入redis userid作为key
redisCache.setCacheObject("LOGIN_USER_KEY:"+loginUserKey, loginUser);
return new ResponseResult(200, "登陆成功", map);

8.前端存储token

image-20231230131958261

  • 前端将后端传回来的token存入sessionStorage中📲

    src\views\Login.vue

1
2
3
4
5
6
7
8
if(data.code==200){
this.loginForm = data.data;
sessionStorage.setItem('token', data.data.token)
console.log(data.data)
this.$message({
message: '登陆成功',
type:'success'
});

至此密钥交换完成,前后端就拥有了共同的SM4公钥,这样前后端就能愉快的加密业务数据了。

项目前后端均有生产SM2密钥对的方法

后端:com/gaomu/utils/crypto/SM2Util.java/generateECSM2HexKey()

前端:src\utils\encrypto\sm2.js\SM2.generateKeyPair()