服务端维护用户登录状态,一般有基于session和token两种方式
一、传统的web项目用session+cookies机制来维护用户登录状态,当然有一定的不足:
- session是保持在服务端内存中的,也就是意味着用户下次请求必须还得在这台服务器上,才能拿到授权的资源,对于分布式系统来说是一个难以处理的点。
- 随着认证用户的增加,服务端的开销也会增大。
二、基于token的鉴权机制
- 客户端拿账号密码请求服务端
- 服务端校验账号密码,如果校验Ok的话,给客户端颁发token
- 客户端拿到服务端颁发的token,后续的请求都会带上这个token
- 服务端根据客户端带的token,对客户端进行鉴权
看完了两种维护用户登录状态方式,接下来看看今天的主角之一:JWT。
JWT是一种开放标准,其声明一般被用来在身份提供者和服务提供者之间传递被认证的用户身份信息,以便于获取服务器的资源。
JWT的构成:
1. 头部header,头部承载两部分信息
{
'typ': 'JWT', //声明类型
'alg': 'HS256'//声明加密算法
}
用base64进行加密得到字符串A
2. 载荷payload,存放有效信息
1)标准中注册的声明
2)公共的声明,一般用来添加用户相关信息,不建议添加敏感信息
3)私有的声明,提供者和消费者共同定义的声明
用base64进行加密得到字符串B
3. 签证,这一部分将base64加密后的header(A)和base64加密后的payload(B)组成的字符串,
通过header中声明的加密方式进行盐secret组合加密,就构成jwt的第三部分(C)
上述三部分用.连接成一个完整的字符串,就构成最终的jwt
至于Spring Secutiry这个框架,大家可以看这一篇文章了解一些相关知识。
Spring Security框架认证详解
接下来看看JWT和Spring Security的一个落地使用
登录流程:
//LoginController
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 首先控制器拦截web请求,并调用loginService方法来获取token
String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
ajax.put(Constants.TOKEN, token);//将获取到的token返回给前端,前端下次就可以拿着这个token来发起请求
return ajax;
}
=======================================
//LoginService
public String login(String username, String password, String code, String uuid)
{
//.......省略部分验证代码
// 用户验证
Authentication authentication = null;
try
{
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
//这里走的是spring security的逻辑
//如果spring security认证成功的话,会返回一个Authentication对象
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
catch (Exception e)
{
}
//通过上面返回的认证对象可以获得用户信息对象
LoginUser loginUser = (LoginUser)authentication.getPrincipal();
//调用tokenService.createToken来生成token,这个token就是jwt生成的
return tokenService.createToken(loginUser);
}
=======================================
//UserDetailsServiceImpl
@Override
//1、首先拿用户名去查询去数据库查询对象
//2、调用createLoginUser来创建一个UserDetails对象并返回
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
SysUser user = userService.selectUserByUserName(username);//自定义查询数据库的逻辑,得到SysUser对象
//省略部分校验代码
return createLoginUser(user);
}
//创建一个UserDetails对象,spring security会拿这个对象和封装的UsernamePasswordAuthenticationToken对象去比对
public UserDetails createLoginUser(SysUser user)
{
return new LoginUser(user, permissionService.getMenuPermission(user));
}
====================================
//生成令牌的操作
//TokenService
public String createToken(LoginUser loginUser)
{
//通过id工具,生成一个随机不重复的字符串
String token = IdUtils.fastUUID();
//将随机字符串保存到loginUser中
loginUser.setToken(token);
refreshToken(loginUser);//将用户信息存放在redis中,并刷新令牌时间
//下面这个map存放的数据,就是到时候存放到JWT载荷payload中
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return createToken(claims);
}
//将用户信息存放到redis中
public void refreshToken(LoginUser loginUser)
{
//设置缓存的过期时间
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 使用之前ID工具生成的不重复字符串作为缓存的key
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
//调用jwt生成token
private String createToken(Map<String, Object> claims)
{
//使用JWT来生成一个最终的token
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
上述就是一个登录的过程的实现,当然想要登录的请求login能被我们自己的controller拦截到
这里还需要做一个配置(spring security自定义拦截配置,这里就不展开细说了)
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
httpSecurity
// 过滤请求
.authorizeRequests()
// 对于登录login 允许匿名访问
.antMatchers("/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
......
}
登录完成后,我们需要明确现在客户端和服务端各自有的状态,客户端拥有服务端颁发的token,服务端的redis存放着一个以token为key的用户对象,并且给这个用户对象设置了过期时间。明确了这个状态,接下来看看客户端是怎么带token来发起请求的
//1、这里的想法是采用一个过滤器,在Spring Security认证流程开始前进行拦截,
//可以通过以下方式配置拦截器 authenticationTokenFilter这个是我们自定义的拦截器
//这里也是spring security的自定义配置
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//2、从请求中获取token,然后根据token去redis中获取,如果能够获取到对象,即刷新redis缓存的过期时间,并把获取到的对象设置到SecurityContextHolder中
//如果没有获取到用户对象,就相当于SecurityContextHolder为空,后续走认证流程的时候就会报错,走Spring Security的异常处理流程
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);