使用JWT实现登录功能
功能实现流程:
1.用户发起登录请求。
2.使用JwtBuilder生成令牌并返回。
3.写一个拦截器,拦截初登录之外的请求。拦截到请求后解析令牌,若正常放行,并将当前用户id存在当前线程。若出异常则返回登陆失败。
实现:
1.引入Jwt令牌依赖
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version>
</dependency>
2.写一个类得到使用jwt所用到的信息、以及一个常量类
配置
sky:jwt:# 设置jwt签名加密时使用的秘钥admin-secret-key: admin# 设置jwt过期时间admin-ttl: 7200000# 设置前端传递过来的令牌名称admin-token-name: token# 设置jwt签名加密时使用的秘钥user-secret-key: user# 设置jwt过期时间user-ttl: 7200000# 设置前端传递过来的令牌名称user-token-name: authentication
得到配置信息
@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {/*** 管理端员工生成jwt令牌相关配置*/private String adminSecretKey;private long adminTtl;private String adminTokenName;/*** 用户端微信用户生成jwt令牌相关配置*/private String userSecretKey;private long userTtl;private String userTokenName;}
常量类,防止写错key
public class JwtClaimsConstant {public static final String EMP_ID = "empId";public static final String USER_ID = "userId";public static final String PHONE = "phone";public static final String USERNAME = "username";public static final String NAME = "name";}
3.可以写一个工具类,其中包括生成和解析两种方法。
public class JwtUtil {/*** 生成jwt* 使用Hs256算法, 私匙使用固定秘钥** @param secretKey jwt秘钥* @param ttlMillis jwt过期时间(毫秒)* @param claims 设置的信息* @return*/public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {// 指定签名的时候使用的签名算法,也就是header那部分SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;// 生成JWT的时间long expMillis = System.currentTimeMillis() + ttlMillis;Date exp = new Date(expMillis);// 设置jwt的bodyJwtBuilder builder = Jwts.builder()// 如果有私有声明,一定要先设置这个自己创建的私有的声明,//这个是给builder的claim赋值,一旦写在标准的声明赋值之后,//就是覆盖了那些标准的声明的.setClaims(claims)// 设置签名使用的签名算法和签名使用的秘钥.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))// 设置过期时间.setExpiration(exp);return builder.compact();}/*** Token解密** @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个* @param token 加密后的token* @return*/public static Claims parseJWT(String secretKey, String token) {// 得到DefaultJwtParserClaims claims = Jwts.parser()// 设置签名的秘钥.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))// 设置需要解析的jwt.parseClaimsJws(token).getBody();return claims;}}
4.可以写一个操作线程类,用于记录当前登录用户
public class BaseContext {public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();public static void setCurrentId(Long id) {threadLocal.set(id);}public static Long getCurrentId() {return threadLocal.get();}public static void removeCurrentId() {threadLocal.remove();}}
5.登录接口生成jwt
@PostMapping("/login") @ApiOperation(value = "员工登录") public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {log.info("员工登录:{}", employeeLoginDTO);Employee employee = employeeService.login(employeeLoginDTO);//登录成功后,生成jwt令牌Map<String, Object> claims = new HashMap<>();//传输的内容,值claims.put(JwtClaimsConstant.EMP_ID, employee.getId());String token = JwtUtil.createJWT(jwtProperties.getAdminSecretKey(),jwtProperties.getAdminTtl(),claims);EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder().id(employee.getId()).userName(employee.getUsername()).name(employee.getName()).token(token).build();return Result.success(employeeLoginVO); }
6.写拦截器,用于处理拦截到的请求
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {@Autowiredprivate JwtProperties jwtProperties;/*** 校验jwt*/public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//判断当前拦截到的是Controller的方法还是其他资源if (!(handler instanceof HandlerMethod)) {//当前拦截到的不是动态方法,直接放行return true;}//1、从请求头中获取令牌String token = request.getHeader(jwtProperties.getAdminTokenName());//2、校验令牌try {log.info("jwt校验:{}", token);Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());log.info("当前员工id:", empId);BaseContext.setCurrentId(empId);//3、通过,放行return true;} catch (Exception ex) {//4、不通过,响应401状态码response.setStatus(401);return false;}}
7.注册拦截器
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {@Autowiredprivate JwtTokenAdminInterceptor jwtTokenAdminInterceptor;@Autowiredprivate JwtTokenUserInterceptor jwtTokenUserInterceptor;/*** 注册自定义拦截器** @param registry*/protected void addInterceptors(InterceptorRegistry registry) {log.info("开始注册自定义拦截器...");registry.addInterceptor(jwtTokenAdminInterceptor).addPathPatterns("/admin/**").excludePathPatterns("/admin/employee/login");registry.addInterceptor(jwtTokenUserInterceptor).addPathPatterns("/user/**").excludePathPatterns("/user/user/login").excludePathPatterns("/user/shop/status");}
}
使用redis+token实现登录功能
功能实现流程:
redis+token的实现和jwt的实现思路基本一样,只不过验证的方法不同
1.用户发起登录请求。
2.使用UUID生成标识存储在redis中,将uuid作为键,对象的json串作为值,并返回uuid。
在存入redis中时,要设置过期时间。
3.写一个拦截器,拦截登录之外的请求。拦截到请求后得到uuid,在redis中查询数据,如果相同则放行,并且重新设置过期时间;不同则返回登陆失败。
4.退出登录时,先查询redis中是否有值,有的话重新设置过期时间(防止操作异常),在最后的操作中删除redis中的uuid键值。
实现(只展示不同的逻辑)
1.登录功能service层
//用户登录public LoginVo login(LoginDto loginDto) {//获取输入验证码和存储的redis的keyString captcha = loginDto.getCaptcha();String key = loginDto.getCodeKey();//根据获取到的redis的key查询redis,String redisCode = redisTemplate.opsForValue().get("user:validate" + key);//比较验证码是否一致//不一样提醒校验失败if(StrUtil.isEmpty(redisCode) || !StrUtil.equalsIgnoreCase(redisCode,captcha)){throw new GuiguException(ResultCodeEnum.VALIDATECODE_ERROR);}//如果一致,删除redis中的验证码redisTemplate.delete("user:validate" + key);//1.获取提交过来的用户名String userName = loginDto.getUserName();//2.根据用户名查找数据库表SysUser sysUser = sysUserMapper.selectUserInfoByUserName(userName);//3.如果根据用户名查找不到对应信息,用户不存在,返回错误信息if(sysUser == null){throw new GuiguException(ResultCodeEnum.LOGIN_ERROR);}//4.如果根据用户名查到用户信息//5.获取输入密码,比较输入的密码和数据库的密码是否一致String database_password = sysUser.getPassword();String input_password = loginDto.getPassword();//把输入的密码进行加密,再比较input_password = DigestUtils.md5DigestAsHex(input_password.getBytes());if(!input_password.equals(database_password)){throw new GuiguException(ResultCodeEnum.LOGIN_ERROR);}//6.登陆成功,生成用户唯一标识String token = UUID.randomUUID().toString().replace("-","");//7.登录成功之后用户信息放入redis中,设置过期时间redisTemplate.opsForValue().set("user:login:" + token, JSON.toJSONString(sysUser),7, TimeUnit.DAYS);//返回loginvo对象LoginVo loginVo = new LoginVo();loginVo.setToken(token);return loginVo;}
2.拦截器
@Component
public class LoginAuthInterceptor implements HandlerInterceptor {@Autowiredprivate RedisTemplate<String , String> redisTemplate ;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取请求方式String method = request.getMethod();if("OPTIONS".equals(method)) { // 如果是跨域预检请求,直接放行return true ;}// 获取tokenString token = request.getHeader("token");if(StrUtil.isEmpty(token)) {responseNoLoginInfo(response) ;return false ;}// 如果token不为空,那么此时验证token的合法性String sysUserInfoJson = redisTemplate.opsForValue().get("user:login:" + token);if(StrUtil.isEmpty(sysUserInfoJson)) {responseNoLoginInfo(response) ;return false ;}// 将用户数据存储到ThreadLocal中SysUser sysUser = JSON.parseObject(sysUserInfoJson, SysUser.class);AuthContextUtil.set(sysUser);// 重置Redis中的用户数据的有效时间redisTemplate.expire("user:login:" + token , 30 , TimeUnit.MINUTES) ;// 放行return true ;}//响应208状态码给前端private void responseNoLoginInfo(HttpServletResponse response) {Result<Object> result = Result.build(null, ResultCodeEnum.LOGIN_AUTH);PrintWriter writer = null;response.setCharacterEncoding("UTF-8");response.setContentType("text/html; charset=utf-8");try {writer = response.getWriter();writer.print(JSON.toJSONString(result));} catch (IOException e) {e.printStackTrace();} finally {if (writer != null) writer.close();}}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {AuthContextUtil.remove(); // 移除threadLocal中的数据}
}
3.退出登录(service层)
public void deleteById(Long userId) {sysUserMapper.deleteById(userId) ;}