碎碎念:
之前写的毕设的登录都就是“select * from user where username = name and password = pwd;”这种很简单的,也没做token校验。这次让我写个登录功能,包括token校验,用threadlocal存储用户信息。
做登录的时候我遇到了很多问题,当然其中也有蛮多傻*问题,就是因为自己绕不清,这次写个文章整理一下。
我遇到的问题:(因为没认真写过登录,就是个小白,所以很多问题都很无脑)
- 密码加密了,我要怎么去库里捞用户了,密码加密不是每次生成的都不一样么?前端传的又是明文还是密文呢?我后端又要对前端解密么?就算捞出用户了,我又怎么校验密码是否正确呢?
- threadlocal要怎么用?
- token咋生成的啊,我丢
首先密码验证,我之前一直想不通,密码加密了我还怎么去库里捞用户,库里存的密文,我咋去比对(就是因为select * from user where username = name and password = pwd;这句sql固化思维了)
这个问题解决方法就是,别带着密码去捞啊!前端提交的表单(username、password)过来,既然username能作为登录凭证就说明username是唯一的呀!那我sql直接写成“select * from user where username = name”不就行了?(我是呆瓜)
(这里前端传的是明文过来,post请求)
那我登录校验的接口就可以写成:
public Boolean login(String username, String password) {
//根据username捞user信息,matches比较明文和user.password
User user = userRepository.findByUsername(username);
if (user != null && passwordEncoder.matches(password,user.getPassword())) {
//【1】
//【2】
return true;
}else {
return false;
}
}
这里密码加密用到的是BCryptPasswordEncoder,弄个配置类:
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
首先流程呢,大概就是这样:
先看第一步,生成token:之前的那个疑问,token要怎么生成,其实token随便你生成,只要保证生成的token是唯一的就行。这里生成token的方式是生成了一个随机的uuid:
@Data
public class AccessToken {
private String accessToken;
public AccessToken(String token) {
this.accessToken = token;
}
public static AccessToken random() {
String token = UUID.randomUUID().toString();
return new AccessToken(token);
}
public static AccessToken of(String accessToken) {
return new AccessToken(accessToken);
}
}
那么token什么时候生成呢,从图上可以看到是登录时,密码校验后生成,所以生成token就放在用户登录接口里面,那么login方法【1】处代码写上:
//生成token
AccessToken accessToken = AccessToken.random();
//返回值换成AccessToken,返回给前端,让前端每次调接口带上这个token
return accessToken;
再看第二步,token已经生成了,那以后要验证token怎么验证呢?要验证我必然要把对的token存起来,才能去判断别人传进来的token对不对,所以要怎么存?不要想的很高大上(我之前就是),不都是那几种数据类型(狗头)?token和用户信息做个键值对,为了方便直接用个map存就行了。
为什么可以用map存?这种要存起来的必然不能随便丢失,而map直接存内存,是个强引用,不会随便被gc,被当垃圾回收掉,只要不计算机重启(项目重启),token都在。
实现:
public interface TokenStore {
void set(AccessToken token, LoginUser user);
LoginUser get(AccessToken token);
}
@Component
public class InMemoryTokenStore implements TokenStore {
private final Map<AccessToken, LoginUser> tokenMap = new ConcurrentHashMap<>();
/**
* 放入token
* @param token 令牌
* @param user 用户信息
*/
@Override
public void set(AccessToken token, LoginUser user) {
tokenMap.put(token,user);
}
/**
* 获取用户信息
* @param token 令牌
* @return 用户信息
*/
@Override
public LoginUser get(AccessToken token) {
return tokenMap.get(token);
}
}
再login方法【2】处加上:
//保存token
LoginUser loginUser = new LoginUser(
user.getId(),
user.getAvatar(),
user.getUsername(),
user.getNickname(),
user.getEmail(),
Collections.singletonList(user.getRole())
);
tokenStore.set(accessToken, loginUser);
第三步,token校验,不止一个接口进来之前需要校验,所以应该要用到拦截器,把需要校验的请求都拦截起来,先过了token校验这一关,再往后面走:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final LoginInterceptor loginInterceptor;
public WebMvcConfig(LoginInterceptor loginInterceptor) {
this.loginInterceptor = loginInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/login/**");
}
}
再加个threalocal,方便其他接口取到用户信息:
public class LoginUserContextHolder {
private static final ThreadLocal<LoginUser> loginUserThreadLocal = new ThreadLocal<>();
public static void set(LoginUser loginUser) {
loginUserThreadLocal.set(loginUser);
}
public static LoginUser get() {
return loginUserThreadLocal.get();
}
public static void remove() {
loginUserThreadLocal.remove();
}
}
什么时候存?
除了登录,基本所有请求都带了token,我们第二步已经把token-用户信息存在map中了,这时候可以取出map的value存到当前线程(threadlocal)中。既然是基本所有请求都需要这一步,那就要写在拦截器中。
取出来:
拦截器:
@Component
public class LoginInterceptor implements HandlerInterceptor {
private final TokenStore tokenStore;
private static final String TOKEN_PREFIX = "Bearer";
public LoginInterceptor(TokenStore tokenStore) {
this.tokenStore = tokenStore;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String authorization = request.getHeader("Authorization");
if (StringUtils.hasText(authorization) && authorization.startsWith(TOKEN_PREFIX)) {
String token = authorization.substring(TOKEN_PREFIX.length() + 1);
LoginUser loginUser = tokenStore.get(AccessToken.of(token));
if (loginUser != null) {
LoginUserContextHolder.set(loginUser);
return true;
}
}
return false;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
LoginUserContextHolder.remove();
}
}