菜单

jwt原理和 认证过程实现

duckflew
发布于 2021-07-30 / 285 阅读
0
0

jwt原理和 认证过程实现

jwt原理和 认证过程实现

一、jwt原理

1.jwt 是什么

全称为 json web token 是一种与自包含,轻量的,服务器分离的json令牌

2.为什么用jwt

传统的认证方式

服务端存储sessionid 在cookie里面 每次请求把cookie带上 一个jsessionid 参数 服务器根据 Jsessionid 找一个session对象 进而可以获取到存储在session里面的 用户信息等 这样做的问题在于 当网站的访问量比较大的时候 服务器需要存储比较多的session 性能会有影响

换用jwt

JWT 简单来说 用户登录之后 服务器返回一个token 这个token包含了用户的一些非敏感信息 一般是用户名 id什么的

这个token 由三个部分组成

header.claim.signature

这三个部分都是由一个json对象 base64 编码得到的

header 的明文 距离如下

{
    typ: "jwt",
    alg: "hmac256"
}

给出了token 的类型和 加密算法 这里说是加密其实不是很准确 因为拿到header 直接base64解密也可以得到这个信息

claim 同理 也是存储一些用户信息

signature就是签名 这个才是关键 这个 签名是通过 header+claims +secret (存储在服务器不能泄露的秘钥) 加密得到 每次 服务器收到token 都会对这个JWT 进行验签的操作

但凡 前两部分的信息有被篡改 后面的签名都会异常

3.一些JWT的问题

同传统的认证不同 JWT 属于拿到令牌就可以和密码解耦

用户修改密码无法影响jwt的有效性 这就导致 服务器其实还是要存储jwt 的相关信息 如此看来 前面jwt 的优势好像又不复存在的 这里引用一个知乎的回答

作者:黎明
链接:https://www.zhihu.com/question/364616467/answer/963172899
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

JWT 是明文签名方案,也就是你在 JWT 中存储的所有东西都是明文(base64 编码)的,加上 secret 使用 HMAC 算法获取摘要签名,通过签名来检查这个 JWT 是否被篡改。

所以 JWT 通常是一次签发,永久有效的。

为了避免这个问题,通常会在 JWT 的数据中放入一个过期时间,虽说是明文放进去的,但是受签名限制,无法篡改。这样,服务端在检查 JWT 是否被篡改的同时检查过期时间,使得这个 JWT 不会永久有效。

但是还有一个问题就是,JWT 在过期时间到达之前,依旧是一直有效的,但这个问题就类似于:你的密码在修改之前是一直有效的。所以“只要拿到 JWT 就可以冒充用户做任何事情了”和“只要拿到用户的密码就可以冒充用户做任何事情了”是一样的……

所以在防止冒充这个部分,你需要其他的防护手段,比如 HTTPS 双向认证,双向加密之类的。

JWT 相比密码,区别在于,JWT 是临时生成的,在有效期内总是有效,超过有效期需要重新生成;而密码是用户设置的,在用户主动修改之前总是有效的,有效期由其他策略控制,但通常都很长,用户修改密码后旧密码立即失效。

所以,如果使用 JWT 做 session 保持,那么在用户修改密码之后,JWT 不会过期,所有持有 JWT 的人依旧可以继续访问。因为 JWT 本身做不到主动失效,相当于 JWT 是用户的一个不受用户控制的“密码”。

如果你的系统支持 JWT 续期,那么一旦 JWT 泄漏,用户将无法找回账号,因为第三方可以拿着截获的 JWT 进行无限续期,用户改密码都不行的。

解决办法就是,在用户退出,或是修改密码的时候,使此用户的所有 JWT 立即失效。前面说过,JWT 一旦生成,在有效期之前总是有效的,是不受控制的……

所以为了使 JWT 立即过期,服务端必须记录所有生成的有效的 JWT,在使用 secret 校验的时候检查 JWT 是否在有效 JWT 列表中,如果不在,那么即便是 JWT 有效也拒绝认证。

这样,只要在用户退出登录,或是修改密码的时候,删除服务端此用户的所有 JWT 记录即可。


但是,到了这个模式,如果服务端要支持用户退出,则必须要记录 JWT 的有效性,而记录 JWT 有效性,不如直接记录一个安全随机数……

也就是说,JWT 本身并不是用来做 session 保持的,session 保持还是用服务端生成的一段安全随机数来进行比较好。

而 JWT 的使用场景,通常是在服务切换的时候,用作身份令牌的。比如从 A 服务跳转到 B 服务,要带过去一些信息,这时候就可以用 JWT 来实现了,JWT 的有效期通常设置为不超过 5 秒。

二、jwt整合SpringSecurity登录流程

1.SpringSecurity工作流程

SpringSecurity的web基础是过滤器 本质上也是通过一层层的过滤器对web请求做处理

一个web请求会经过一条过滤器链,在经过过滤器链的过程中会完成认证与授权,如果中间发现这条请求未认证或者未授权,会根据被保护API的权限去抛出异常,然后由异常处理器去处理这些异常。

工作流程图如下

image-20210514210839417

个请求想要访问到API就会以从左到右的形式经过蓝线框框里面的过滤器,其中绿色部分是我们本篇主要讲的负责认证的过滤器,蓝色部分负责异常处理,橙色部分则是负责授权。

如果你用过Spring Security就应该知道配置中有两个叫formLoginhttpBasic的配置项,在配置中打开了它俩就对应着打开了上面的过滤器。

  • formLogin对应着你form表单认证方式,即UsernamePasswordAuthenticationFilter。
  • httpBasic对应着Basic认证方式,即BasicAuthenticationFilter。

image-20210514211006025

换言之,你配置了这两种认证方式,过滤器链中才会加入它们,否则它们是不会被加到过滤器链中去的。

2.SpringSecurity的重要概念

知道了Spring Security的大致工作流程之后,我们还需要知道一些非常重要的概念也可以说是组件:

  • SecurityContext:上下文对象,Authentication对象会放在里面。
  • SecurityContextHolder:用于拿到上下文对象的静态工具类。
  • Authentication:认证接口,定义了认证对象的数据形式。
  • AuthenticationManager:用于校验Authentication,返回一个认证完成后的Authentication对象。
Authencation

Authencation类的几个方法如下

  • getAuthorities: 获取用户权限,一般情况下获取到的是用户的角色信息

  • getCredentials: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。

  • getDetails: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)。

  • getPrincipal: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails。

  • isAuthenticated: 获取当前 Authentication 是否已认证。

  • setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。

  • getAuthorities: 获取用户权限,一般情况下获取到的是用户的角色信息

  • getCredentials: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。

  • getDetails: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)。

  • getPrincipal: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails。

  • isAuthenticated: 获取当前 Authentication 是否已认证。

  • setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。

Authentication只是定义了一种在SpringSecurity进行认证过的数据的数据形式应该是怎么样的,要有权限,要有密码,要有身份信息,要有额外信息。

AuthenticationManager

AuthenticationManager定义了一个认证方法,它将一个未认证的Authentication传入,返回一个已认证的Authentication,默认使用的实现类为:ProviderManager。

将这四个部分,串联起来,构成Spring Security进行认证的流程:

\1. 👉先是一个请求带着身份信息进来
\2. 👉经过AuthenticationManager的认证,
\3. 👉再通过SecurityContextHolder获取SecurityContext
\4. 👉最后将认证后的信息放入到SecurityContext

3.一些必要的组件

1.密码加密器Bean

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

这个Bean是不必可少的,Spring Security在认证操作时会使用我们定义的这个加密器,如果没有则会出现异常。当然了 这只是实现类中的一个

2.AuthenticationManager

@Bean    
public AuthenticationManager 	authenticationManager() throws Exception
{        
    return super.authenticationManager();   
}

3.实现UserService

这里以我做的一个项目为例

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
    Admin admin=adminRepository.findByUsername(username);
    if (admin==null)throw new UsernameNotFoundException(username);
    List<GrantedAuthority> authorities=new ArrayList<>();
    for (Role adminRole : admin.getAdminRoles())
    {
        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(adminRole.getName());
        authorities.add(simpleGrantedAuthority);
    }
    admin.setAuthorities(authorities);
    return admin;
}

AdminService这个类实现UserDetailsService接口 重写上述方法 返回admin 这个admin也重写了UserDetails接口

4. TokenUtil

由于我们是JWT的认证模式,所以我们也需要一个帮我们操作Token的工具类,一般来说它具有以下三个方法就够了:

  • 创建token
  • 验证token
  • 反解析token中的信息
package com.msgf.hr.utils;

import com.msgf.hr.pojo.AccessToken;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Calendar;
import java.util.Date;

@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
public class JwtTokenUtil
{
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.tokenHeader}")
    private String tokenRequestHeader;

    /**
     * 生成一个token
     * accessToken类设置了两个属性
     * String token;
     * Date expirationTime;
     * @param subject
     * @return
     */
    public AccessToken generateToken(String subject)
    {
        Calendar instance=Calendar.getInstance();
        instance.add(Calendar.DATE,3);
        Date expirationTime=instance.getTime();
        AccessToken accessToken=new AccessToken();

        String token=Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(new Date())
                .setExpiration(expirationTime)
                .signWith(SignatureAlgorithm.HS256,secret)
                .compact();
        accessToken.setExpirationTime(instance.getTime());
        accessToken.setToken(token);
        return accessToken;
    }

    /**
     * 解析token 拿到对应的信息  这里主要是用户名  如果签名不对会报错
     * @param token
     * @return
     */
    private Claims getClaimsFromToken(String token)
    {
        Claims claims;
        try
        {
           claims=  Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        }catch (Exception e)
        {
            claims=null;
            System.out.println("jwt 解析失败");
        }
        return claims;
    }
    public AccessToken generateToken(UserDetails userDetails)
    {
        return generateToken(userDetails.getUsername());
    }
    public boolean tokenIsExpired(String token)
    {
        try
        {
            Claims claims=getClaimsFromToken(token);
            Date expiration=claims.getExpiration();
            return expiration.before(new Date());
        }
        catch (Exception e)
        {
            return false;
        }
    }

    /**
     * 刷新token
     * @param token
     * @return
     */
    public AccessToken refreshToken(String token)
    {
        AccessToken newToken;
        try
        {
            Claims claims=getClaimsFromToken(token);
            newToken=generateToken(claims.getSubject());
        }catch (Exception e)
        {
            newToken=null;
            e.printStackTrace();
        }
        return newToken;
    }

    /**
     * 从token获取用户名
     * @param token
     * @return
     */
    public String getUsernameFromToken(String token)
    {
        String username;
        try
        {
            Claims claims=getClaimsFromToken(token);
            username=claims.getSubject();
        }
        catch (Exception e)
        {
            username=null;
            e.printStackTrace();
        }
        return username;
    }

    /**
     * 验证token是否有效
     * @param token
     * @param userDetails
     * @return
     */
    public boolean validateToken(String token,UserDetails userDetails)
    {
        Claims claims=getClaimsFromToken(token);
        return claims.getSubject().equals(userDetails.getUsername())&&!tokenIsExpired(token);
    }

    /**
     * 从请求头中拿到前端传来的等待校验的token
     * @param req
     * @return
     */
    public String getToken(HttpServletRequest req)
    {
        return req.getHeader(tokenRequestHeader);
    }
}

4.JWT过滤器

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    @Autowired
    JwtTokenUtil jwtTokenUtil;
    @Autowired
    AdminServiceImpl adminService;
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) throws ServletException, IOException
    {
        String authToken= jwtTokenUtil.getToken(req);
        if (StrUtil.isNotEmpty(authToken))
        {
            String loginAccountUsername=jwtTokenUtil.getUsernameFromToken(authToken);
            if (StrUtil.isNotEmpty(loginAccountUsername)&& SecurityContextHolder.getContext().getAuthentication()==null)
            {
                UserDetails userDetails=adminService.loadUserByUsername(loginAccountUsername);
                if (userDetails!=null&&jwtTokenUtil.validateToken(authToken,userDetails))
                {
                    UsernamePasswordAuthenticationToken authentication=new UsernamePasswordAuthenticationToken(userDetails,userDetails.getPassword(),userDetails.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                    System.out.println("token正确  自动登录成功"+userDetails.getUsername());
                }
            }
        }
        chain.doFilter(req,resp);
    }
}

5.把过滤器配置到 系统中

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter
{
    @Bean
    public PasswordEncoder passwordEncoder()
    {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public AuthenticationManager authenticationManager() throws Exception
    {
        return super.authenticationManager();
    }
    @Bean
    ApiAccessDeniedHandler apiAccessDeniedHandler()
    {
        return new ApiAccessDeniedHandler();
    }
    @Bean
    ApiAuthenticationEntryPoint apiAuthenticationEntryPoint()
    {
        return new ApiAuthenticationEntryPoint();
    }
    @Autowired
    AdminServiceImpl adminService;

    @Bean
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter()
    {
        return new JwtAuthenticationTokenFilter();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        http.authorizeRequests()
        .antMatchers(HttpMethod.OPTIONS).permitAll()  //放行所有option请求
        .antMatchers("/auth/login","/auth/logout"
        ,"/captcha").permitAll()
        .anyRequest().authenticated()
        .and().exceptionHandling()
            .accessDeniedHandler(apiAccessDeniedHandler())
            .authenticationEntryPoint(apiAuthenticationEntryPoint())
        .and()
                .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)
                .cors()
        .and()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

6.添加两个异常处理器

public class ApiAccessDeniedHandler implements AccessDeniedHandler
{
    @Override
    public void handle(HttpServletRequest req, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException
    {
        e.printStackTrace();
        resp.setHeader("Cache-Control","no-cache");
        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("application/json");
        resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
        resp.getWriter().println("权限不足 访问拒绝");
        resp.getWriter().flush();
    }
}

public class ApiAuthenticationEntryPoint implements AuthenticationEntryPoint
{
    @Override
    public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e)
            throws IOException, ServletException
    {
        e.printStackTrace();
        resp.setHeader("Cache-Control", "no-cache");
        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("application/json");
        resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        resp.getWriter().println("非法访问");
        resp.getWriter().flush();
    }
}

评论