Java教程

循序渐进学spring security 第十篇 如何用token登录?JWT闪亮登场

本文主要是介绍循序渐进学spring security 第十篇 如何用token登录?JWT闪亮登场,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

  • JWT简介
  • JWT认证
  • JWT的结构
  • JWT登录流程
  • 如何引入JWT
  • JwtHelper 工具类介绍
    • 如何创建token?
    • 如何解析token?
  • 创建项目
    • 创建项目:security-mybatis-jwt
    • 引入JWT maven依赖
    • 将登录成功,登录失败,登出等配置的handler抽取出来单独放在一个handler包
    • 创建JWT工具类
    • 登录成功后生产token返回给前端
    • 如何校验token的有效性?
  • 验证失败的处理
  • 启动测试

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

这篇关于循序渐进学spring security 第十篇 如何用token登录?JWT闪亮登场的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!