使用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的body
JwtBuilder 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) {
// 得到DefaultJwtParser
Claims 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 {
@Autowired
private 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 {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
@Autowired
private 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的key
String 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 {
@Autowired
private RedisTemplate<String , String> redisTemplate ;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求方式
String method = request.getMethod();
if("OPTIONS".equals(method)) { // 如果是跨域预检请求,直接放行
return true ;
}
// 获取token
String 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();
}
}
@Override
public 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) ;
}