token登录 threadlocal存储当前用户信息

碎碎念:
之前写的毕设的登录都就是“select * from user where username = name and password = pwd;”这种很简单的,也没做token校验。这次让我写个登录功能,包括token校验,用threadlocal存储用户信息
做登录的时候我遇到了很多问题,当然其中也有蛮多傻*问题,就是因为自己绕不清,这次写个文章整理一下。

我遇到的问题:(因为没认真写过登录,就是个小白,所以很多问题都很无脑)

  1. 密码加密了,我要怎么去库里捞用户了,密码加密不是每次生成的都不一样么?前端传的又是明文还是密文呢?我后端又要对前端解密么?就算捞出用户了,我又怎么校验密码是否正确呢?
  2. threadlocal要怎么用?
  3. 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登录 threadlocal存储当前用户信息_第1张图片
先看第一步,生成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();
    }

}

你可能感兴趣的:(java)