Token方式 验证登录

Token 验证

在这之前先简单讲下登陆中验证码认证过程,先是在WebSecurityConfig里面配置下生成验证码的路径,使用谷歌的com.google.code.kaptcha API中的Producer对象生成验证码,后台生成Cookie 对象,key可以自定义,值用uuid去生成,然后把验证码存到redis中(key用的刚才uuid生成的那个值,value存验证码),然后把Cookie 返给前端。登录的时候传验证码过来,并且把刚才返的Cookie带过来,校验就根据cookie从redis中去取验证码。

public void captcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setHeader(“Cache-Control”, “no-store, no-cache”);
response.setContentType(“image/jpeg”);

    //生成文字验证码
    String text = producer.createText();
    //生成图片验证码
    BufferedImage image = producer.createImage(text);
    //保存session
    String uuid = UUID.randomUUID().toString();
    Cookie cookie = new Cookie(SystemConstants.KAPTCHA_SESSION_KEY, uuid);
    response.addCookie(cookie);
    redisUtils.set(uuid, text);
    ServletOutputStream out = response.getOutputStream();
    ImageIO.write(image, "jpg", out);
}

使用基于 Token 的身份验证方法,大概的流程是这样的:

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
  4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

总的来说就是客户端在首次登陆以后,服务端再次接收http请求的时候,就只认token了,请求只要每次把token带上就行了,服务器端会拦截所有的请求,然后校验token的合法性,合法就放行,不合法就返回401(鉴权失败)。

乍的一看好像和前面的seesion-cookie有点像,seesion-cookie是通过seesionid来作为浏览器和服务端的链接桥梁,而token验证方式貌似是token来起到seesionid的角色。其实这两者差别是很大的。

  1. sessionid 他只是一个唯一标识的字符串,服务端是根据这个字符串,来查询在服务器端保持的seesion,这里面才保存着用户的登陆状态。但是token本身就是一种登陆成功凭证,他是在登陆成功后根据某种规则生成的一种信息凭证,他里面本身就保存着用户的登陆状态。服务器端只需要根据定义的规则校验这个token是否合法就行。
  2. session-cookie是需要cookie配合的,居然要cookie,那么在http代理客户端的选择上就是只有浏览器了,因为只有浏览器才会去解析请求响应头里面的cookie,然后每次请求再默认带上该域名下的cookie。但是我们知道http代理客户端不只有浏览器,还有原生APP等等,这个时候cookie是不起作用的,或者浏览器端是可以禁止cookie的(虽然可以,但是这基本上是属于吃饱没事干的人干的事)…,但是token 就不一样,他是登陆请求在登陆成功后再请求响应体中返回的信息,客户端在收到响应的时候,可以把他存在本地的cookie,storage,或者内存中,然后再下一次请求的请求头重带上这个token就行了。简单点来说cookie-session机制他限制了客户端的类型,而token验证机制丰富了客户端类型。
  3. 时效性。session-cookie的sessionid实在登陆的时候生成的而且在登出事时一直不变的,在一定程度上安全就会低,而token是可以在一段时间内动态改变的。
  4. 可扩展性。token验证本身是比较灵活的,一是token的解决方案有许多,常用的是JWT,二来我们可以基于token验证机制,专门做一个鉴权服务,用它向多个服务的请求进行统一鉴权。

下面就拿最常用的JWT(JSON WEB TOKEN)来说:

JWT是Auth0提出的通过对JSON进行加密签名来实现授权验证的方案,就是登陆成功后将相关信息组成json对象,然后对这个对象进行某中方式的加密,返回给客户端,客户端在下次请求时带上这个token,服务端再收到请求时校验token合法性,其实也就是在校验请求的合法性。

具体实现如下:

调用登录接口,先是进入拦截器匹配url,

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

    try {
        String servletPath = request.getServletPath();
        boolean isLogin= pathMatcher.match("/login", servletPath);
        if (isNoPermission(servletPath,"/swagger-ui.html","/swagger-resources/**","/v2/**",             "/webjars/**","/api/voucher/download/**","/project/sourceAttach/download/**")){
            filterChain.doFilter(request, response);
            return;
        }
        if(!isLogin){

            boolean isCaptcha=pathMatcher.match("/code/**", servletPath);

            if(!isCaptcha){
                validToken(request,response);
            }
        }else{
            /**
             * 验证码校验
             */
            if (!"dev".equals(environment)){
                validKaptcha(request);
            }
        }

    }catch (BadCredentialsException e){
        authenticationFailureHandler.onAuthenticationFailure(request,response,e);
        return;
    }

    filterChain.doFilter(request, response);

}

通过后就进行调用登陆器

/**

  • 自定义登录登录器,使用JSON登录
    */
    public class LoginAuthFilter extends UsernamePasswordAuthenticationFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(LoginAuthFilter.class);

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

     if ( !request.getMethod().equals("POST")) {
         throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
     }
    
        String  username = this.obtainUsername(request);
        String password = this.obtainPassword(request);
    
         if (username == null) {
             username = "";
         }
    
         if (password == null) {
             password = "";
         }
         
     username = username.trim();
     UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
     this.setDetails(request, authRequest);
     return this.getAuthenticationManager().authenticate(authRequest);
    

    }

接着进行登录认证

/**

  • 登录认证逻辑
    */
    public class LoginAuthenticationManager implements AuthenticationManager {

    private static final Logger LOGGER = LoggerFactory.getLogger(LoginAuthenticationManager.class);

    private UserService userService;

    public LoginAuthenticationManager(UserService userService){
    this.userService=userService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

     if(authentication.getPrincipal() == null || authentication.getCredentials() == null) {
         throw new BadCredentialsException("登陆验证失败,用户名或密码为空");
     }
    
     String username = (String)authentication.getPrincipal();
     //查询用户信息
     User user=userService.loadUserByUsername(username);
     if(user==null){
         throw new BadCredentialsException("账号信息不存在");
     }
     String password = (String)authentication.getCredentials();
    
     Boolean passwordTrue=new BCryptPasswordEncoder().matches(password,user.getPassword());
     //比较密码是否一致
     if(!passwordTrue){
         throw new BadCredentialsException("密码不正确");
     }
     return new UsernamePasswordAuthenticationToken(user, authentication.getCredentials(), user.getAuthorities());
    

    }
    }

认证通过后就进入了自定义的 登录认证成功的handler进行生成token操作,并把token存进redis,然后返回给前端,这里还可以把用户相关信息封装好根据需要返给前端

@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {

    User sysUserEntity=(User) authentication.getPrincipal();
    User sysUser=new User();
    sysUser.setUserId(sysUserEntity.getUserId());
    sysUser.setUserName(sysUserEntity.getUserName());
    sysUser.setLoginUserName(sysUserEntity.getLoginUserName());
    //创建token
    String token= JwtTokenUtil.createJWT(sysUserEntity);

    redisUtils.set(token+sysUserEntity.getUserId(),token);

    //设置一下用户登录信息到redis中去
    redisUtils.set(sysUserEntity.getUserId(), JSON.toJSONString(sysUserEntity));

    JSONObject data = new JSONObject();
    data.put("code", SystemConstants.SUCCESS);
    data.put("msg", "登陆成功");
    data.put("data", sysUser);
    httpServletResponse.setHeader(SystemConstants.AUTHORIZATION,token);
    response(httpServletResponse, data);
}

对应的生成token逻辑,这里使用jwt生成的

/**
* 构建jwt
* @param user
* @param
* @return
*/
public static String createJWT(User user) {
try {
// 使用HS256加密算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        //生成签名密钥
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(BASE64_BIARY);
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());


        //添加构成JWT的参数
        JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
                // 可以将基本不重要的对象信息放到claims
                .setSubject(user.getUserName())           // 代表这个JWT的主体,即它的所有人
                .setIssuedAt(new Date())        // 是一个时间戳,代表这个JWT的签发时间;
                .setClaims(accountEntityfillMap(user))
                .signWith(signatureAlgorithm, signingKey);
        //生成JWT
        return builder.compact();
    } catch (Exception e) {
        log.error("签名失败", e);
        throw e;
    }
}

public static Map<String, Object> accountEntityfillMap(User user) throws NullPointerException {
    Map<String, Object> info = new WeakHashMap<>();
    info.put("loginUserName", user.getLoginUserName());
    info.put("userId", user.getUserId());
    info.put("exTime",new Date());
    return info;
}

然后前端把登陆成功返的token存起来,后面每次调用接口的时候带上就可以了,调用接口的时候会先进拦截器,然后就是校验token逻辑
private void validToken(HttpServletRequest request, HttpServletResponse response) throws BadCredentialsException{

    boolean hasPower = false;

    //获取请求头
    String token=request.getHeader(SystemConstants.AUTHORIZATION);

    if(ObjectMapper==null){
        ObjectMapper = new ObjectMapper();
    }

    try {

        if(token ==null){
            throw new BadCredentialsException("token is empty");
        }
        //解析token
        Claims claims=JwtTokenUtil.parseJWT(token);

        String userId=claims.get("userId",String.class);



        if (userId == null || SecurityContextHolder.getContext().getAuthentication() == null) {

            //判断是否过期
            if(redisUtils.get(userId)==null){
                throw new BadCredentialsException("token 已过期");
            }

            //获取redis中的用户信息
            String userJson=redisUtils.get(userId);

            User user = JSON.parseObject(userJson,User.class);

            //判断权限
            if(user!=null && CollectionUtils.isEmpty(user.getPermissions())){
                throw new BadCredentialsException("当前请求没有权限,请联系管理员");
            }

            if(!hasPower && user.getPermissions() != null) {
                for (Permission ga : user.getPermissions()) {
                    AntPathRequestMatcher matcher = new AntPathRequestMatcher(ga.getUri());
                    if (hasPower=matcher.matches(request)) {
                        break;
                    }
                }
            }


            if(!hasPower) { //当前请求没有权限
                throw new BadCredentialsException("当前请求没有权限,请联系管理员");
            }

            UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                //设置为已登录
                SecurityContextHolder.getContext().setAuthentication(authentication);

        }
        }catch (BadCredentialsException e){
        log.error("拦截token验证", e);
       // ExceptionPrinterUtil.instance().write(response, e.getMessage(), "UTF-8");
        throw new BadCredentialsException(e.getMessage());
    }
}

/**
* 解析jwt
* @param jsonWebToken
* @param
* @return
*/
public static Claims parseJWT(String jsonWebToken) {
try {
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(BASE64_BIARY))
.parseClaimsJws(jsonWebToken).getBody();
return claims;
} catch (ExpiredJwtException eje) {
log.error("===== Token过期 =", eje);
throw eje;
} catch (Exception e){
log.error("
= token解析异常 =====", e);
throw e;
}
}

token校验通过后,就可以访问具体接口,请求数据,并返给前端

  • 4
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值