循序渐进学spring security 第十篇 如何用token登录?JWT闪亮登场
作者:互联网
文章目录
JWT简介
JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。
JWT认证
我理解的,就是用户登录成功后,服务端会给客户端发放凭证token,并且凭证token不再由服务端保存,而是由客户端自己保存。即用户登陆后,将加密登陆凭证交于客户端,客户端并不明白凭证有何意义,只知道登陆需要使用。在访问时服务端获取到登陆凭证token进行解密,获取到当前用户信息。同时用户凭证有一定的实效,当超过一定时效后,将会失效
JWT的结构
JWT包含了使用 . 分隔的三部分:
1.Header 头部,包含了两部分:token类型和采用的加密算法。
2.Payload 负载,Token的第二部分是负载,它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据。
3.Signature 签名,创建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。
JWT 生成的token如下
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHBpcmVzSW4iOjE2Mjc3MzY1MzkzNTQsImF1dGhvcml0aWVzIjpbeyJhdXRob3JpdHkiOiJST0xFX3VzZXIifV0sImVuYWJsZWQiOnRydWUsInVzZXJuYW1lIjoibWlrZSJ9.DCoCxd8lIRtzntLdjLPHB-utL8xZttnFQkTFny8MKZI
虽然JWT包含了几个部分,但是我们也不需要关心太多,毕竟,现在的JWT比较成熟,工具类很友好,只需处理我们关系的部分就可以了,也就是claim,而工具类已经封装好了,我们直接取出来解析好就可以直接用
JWT登录流程
1.用户在浏览器访问资源
2.服务端取出token进行校验
3.如果token有效,取出用户信息(就是将用户信息保存到SecurityContextHolder.getContext()上下文中)
4.如果token失效或者没有携带token,则走登录逻辑
5.登录成功后,服务端创建登录凭证token,返回给客户端(服务端不保存token)
6.客户端浏览其他资源,携带token,继续走1,2逻辑
OK,大致了解了JWT,我们用demo来介绍
如何引入JWT
只需要在项目中添加依赖即可:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
<scope>compile</scope>
</dependency>
JwtHelper 工具类介绍
其实,很多工作JwtHelper类已经帮我们封装好了,我们可以直接拿来用
如何创建token?
Map<String, Object> claums=new HashMap<>();
claums.put("username","mike");
claums.put("mobile","15994643438");
claums.put("expires_in",14400000);
MacSigner rsaSigner=new MacSigner(secret);
Jwt encode = JwtHelper.encode(JSON.toJSONString(claums), rsaSigner
String token = encode.getEncoded();
System.out.println(token);
这里,secret 是加密串,可以自由定义
如何解析token?
Jwt decode = JwtHelper.decode(token);
System.out.println(decode.getClaims());
这样,解析出来的就是一个Jwt对象,可以直接获取到Claims,Claims就是我们登录成功后存储在token中的信息
运行结果:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtb2JpbGUiOiIxNTk5NDY0MzQzOCIsImV4cGlyZXNfaW4iOjE0NDAwMDAwLCJ1c2VybmFtZSI6Im1pa2UifQ.DOgTvXN1FdJ12OWmezzCJ9vkjU0Flh6CsCenpqb7mXE
{"mobile":"15994643438","expires_in":14400000,"username":"mike"}
是不是很简单?
我们结合spring security 来搞个demo,按需求来做吧,简单的一个需求:
某企业要做前后端分离的项目,决定要用spring boot + spring security+JWT 框架实现登录认证授权功能,用户登录成功后,服务端利用JWT生成token,之后客户端每次访问接口,都需要在请求头上添加Authorization:Bearer token 的方式传值到服务器端,服务器端再从token中解析和校验token的合法性,如果合法,则取出用户数据,保存用户信息,不需要在校验登录,否则就需要重新登录
好了,基于上述需求,我们来新建项目
创建项目
创建项目:security-mybatis-jwt
我们基于上一篇文章《循序渐进学spring security 第七篇,如何基于用户表和权限表配置权限?越学越简单了》的项目copy一个新的项目,改名为:security-mybatis-jwt
引入JWT maven依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.9.RELEASE</version>
<scope>compile</scope>
</dependency>
将登录成功,登录失败,登出等配置的handler抽取出来单独放在一个handler包
创建JWT工具类
也可以直接用JwtHelper, 我这里为了方便管理,还是稍微封装了一下
public class JWTUtils {
/**
* 创建JWT
* @param secret
* @param claims 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
* @return
*/
public static String getAccessToken(String secret, Map<String, Object> claims){
// 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
MacSigner rsaSigner=new MacSigner(secret);
Jwt jwt = JwtHelper.encode(JSON.toJSONString(claims), rsaSigner);
return jwt.getEncoded();
}
public static Map<String,Object> parseToken(String token){
Jwt jwt = JwtHelper.decode(token);
return JSON.parseObject(jwt.getClaims());
}
/**
* 根据传入的token过期时间判断token是否已过期
* @param expiresIn
* @return true-已过期,false-没有过期
*/
public static boolean isExpiresIn(long expiresIn){
long now=System.currentTimeMillis();
return now>expiresIn;
}
}
这里,封装了三个方法
- getAccessToken 获取token,第一个参数可以是任意,第二个参数,就是需要封装到token里面的用户信息claims
- 解析token,或得到claims数据,转换为Map集合
- 校验token的时效是否过期
登录成功后生产token返回给前端
登录成功后,会走UsernamePasswordAuthenticationSuccessHandler 的onAuthenticationSuccess 方法,如果不熟悉的可以翻看我之前的文章《循序渐进学习spring security 第五篇,如何处理重定向和服务器跳转?登录如何返回JSON串》,我们在这方法里面,生成token,并以JSON串形式返回给前端
@Component
public class UsernamePasswordAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Value("${harry.jwt.secret: huangxuanheng@163.com}")
private String secret;
@Value("${harry.jwt.expMillis: 7200000}")
private long expMillis;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Object principal = authentication.getPrincipal();
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
User user= (User) principal;
//生成token
Map<String, Object> claims=new HashMap<>();
claims.put("username",user.getUsername());
claims.put("authorities",user.getAuthorities());
claims.put("enabled",user.isEnabled());
claims.put("expiresIn",(System.currentTimeMillis()+expMillis));
String token = JWTUtils.getAccessToken(secret, claims);
Map<String,Object>result=new HashMap<>();
result.put("accessToken",token);
out.write(JSON.toJSONString(result));
out.flush();
out.close();
}
}
- secret 和 expMillis 是从配置文件读取的,定义在配置文件中
harry:
jwt:
secret: e9948PG02lURjvhjotDGQ6ksRdz3920MEfdy0q6HIszaxNNXw5D1yGq7l3zVWVfUbPBSA56JMqawy7Mt2vPDx5AveuOHHpT0uZB #随机生成,可在百度上自行搜索,或者自己取随机字符串
# expMillis: 14400000 #4个小时候过期,可根据实际情况自行修改
expMillis: 20000 #4个小时候过期,可根据实际情况自行修改
- 从authentication 取出用户信息,保存到claims map对象
- 然后再使用JWT工具生成token
- 最后将token以JSON串返回前端
如何校验token的有效性?
这一步,我们需要写一个过滤器:JwtAuthenticationTokenFilter,继承OncePerRequestFilter,在这个过滤器里面实现doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 方法,取出客户端在请求头上携带的token数据,进行解析校验
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private String tokenHead = "Bearer ";
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String tokenValue = request.getHeader(HttpHeaders.AUTHORIZATION);
if(!StringUtils.hasText(tokenValue)){
filterChain.doFilter(request,response);
return;
}
String token = tokenValue.substring(tokenHead.length());
Map<String, Object> parseJWT = JWTUtils.parseToken(token);
if(JWTUtils.isExpiresIn((long)parseJWT.get("expiresIn"))){
//token 已经过期
SecurityContextHolder.getContext().setAuthentication(null);
filterChain.doFilter(request,response);
return;
}
String username = (String) parseJWT.get("username");
if(StringUtils.hasText(username)&& SecurityContextHolder.getContext().getAuthentication() == null){
//正常用户
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if(userDetails!=null&&userDetails.isEnabled()){
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//设置用户登录状态
log.info("authenticated user {}, setting security context",username);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request,response);
}
}
我们这里说下实现的方法
- 首先,从请求头中取出token,进行判断,如果没有携带token,则继续往下走其他的其他的filter逻辑
- 将token割除前缀“Bearer ”,然后使用封装的JWT工具解析token,得到一个map对象
- 取出token中的过期时间,调用JWT工具中封装的过期时间校验,如果token已经过期,则删除登录的用户,继续往下走其他filter逻辑
- 取出token中的用户名,继续查询数据库,确保状态OK后,保存用户信息到SecurityContextHolder.getContext().setAuthentication(authentication); 这样就完成了token的校验
验证失败的处理
public class UsernamePasswordAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.setHeader("WWW-Authenticate", "Bearer");
response.addHeader("Access-Control-Allow-Origin", "*");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter out = response.getWriter();
Map<String,Object> data = new HashMap<>();
data.put("path", request.getRequestURI());
data.put("time", LocalDateTime.now().toString());
data.put("errCode", HttpStatus.UNAUTHORIZED.value());
data.put("errMsg", HttpStatus.UNAUTHORIZED.getReasonPhrase());
out.write(JSON.toJSONString(data));
out.flush();
out.close();
}
}
如果验证失败,统一返回JSON串,并将状态码设置为401,表示未授权
启动测试
启动项目,登录测试
登录成功后返回token
然后拿token放到请求头,访问其他接口
20秒后,token会过期,过期后访问是这样
和我们预期的一样,这样,我们就完成了JWT生成用户凭证token的介绍了
源码下载 项目security-mybatis-jwt
标签:jwt,登录,spring,第十篇,JWT,token,security,response 来源: https://blog.csdn.net/huangxuanheng/article/details/119281503