session机制到token机制到jwt实践应用

17 篇文章 0 订阅

   从开始的cookie在web间做信息认证,到session机制,紧接着因为session带来的跨域问题,和越来越多的前后端分离、分布式项目,更多的开发者选择了自己喜爱的token机制,以及jwt(JSON web token),本文会从cookie开始,一步一步介绍到jwt的使用。

目录

          cookie的简单介绍

          session机制带来了什么

          为什么要用token?

​          jwt使用。

         jwt实战应用:


          cookie的简单介绍


  cookie 存储在浏览器中。 浏览器 js可以使用document.cookie获取cookie信息 。 相当于给访问用户提供一个通行证。 而session则相当于是在服务器上存储一份用户档案,不过相应服务器压力也会增加。

cookie大小4k; cookie默认是在浏览器关闭后失效,也可以通过expires设置时间  保存在本地硬盘里;
cookie注入

 有的网站没有对cookie做特殊处理,那么就可以线使用 xss获取到cookie, 然后用chrome上的插件cookieheacker进行注入绕过登录。

          session机制带来了什么

session 机制  : session是基于cookie的。  session保存在服务器上,cookie保存在客户端浏览器。 流程大致是,用户A从浏览器登录访问了服务器,服务器会在session 的map中存储key为session_id_A,value 为value_a的键值对,而用户B访问过该服务器后,也同样能存储,即session最后会保存为{session_id_A,value_a},:{session_id_B,value_b}...的map形式中。用户A在访问服务器后得到session_id_A后存储在浏览器中的cookie中,并且每次登录一次,就会将对应的session活跃一次。


session中保存的信息尽量精简,负责各个用户的信息都保存起来会很占用内存,大量请求造成内存泄露。

那假如说浏览器不支持cookie呢 ,很多手机上就不支持,而session是基于cookie的 ,怎么办? 这里就可以使用url地址重写。在重写的地址url后面添加sessionId.

session持久化  session保存在数据库中
使用session鸡肋:  cookie和seesion必须是在同一域名下有效,为啥呢?就好比浏览器得到了百度的cookie_bd,又访问了谷歌,得到cookie_google。那谷歌可以去篡改百度域名下的cookie_bd吗,肯定不行  这就是他的不可跨域名性。跨域导致请求无法携带和服务器对应的cookie ,这也意味着你自己开发的项目前后端必须是在一台服务器上 ???  说好的要搞前后端分离呢,所以session-cookie机制只能处理同源,对于不同域下的前后端就得想法子了,常见的nginx反向代理 、jsonp、CORS跨域资源共享,当然token也可以

 

          为什么要用token?

      在使用token时候,服务器只需要存储一个密钥, 而将随机的字符串token返回给浏览器,浏览器在localstorage /sessionstorage /cookie 中存储, 只需要存密钥不方便吗,真香 
      在需要向后台请求接口时,在请求头添加: authorization:token。因为可以不依赖cookie,就可以避免CSRF跨站伪造攻击 ,多用于restful api请求。

token的组成 : 加密信息+用户信息+生成签名 ,在jwt中(JSON web token)中形式为 header.payload.signature  

token 认证过程
    首次登陆,后端登陆认证成功后,生成token签名,随response返回给前端。前端将token 存储在localstorage中,前端页面在每次页面跳转时候,判断localstorage中是否有token,没有则定向到login页面,有则登陆成功。后台访问接口时候,后端根据token有无判断,以及是否过期,是否被篡改,返回200 或者401.前端获取到401时候重定向到登录页面。
是否被篡改,就需要根据服务器端保存的密钥进行判断。


        jwt使用。

jwt(json web token)常用于生成token,并包含对应的校验。下面进行详细介绍:
jwt 签发原理如下 ,

jwt总共包含三部分
header {"type":"JWT","alg":"HS256"} ; 载荷payload{"userId":021,"book":"月亮与六便士"};签名signature 
第一部分:

使用base64对头部header编码,header包含了加密方式,这里是hs256,编码后为eyJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ==   组成jwt的第一部分;

第二部分:
载荷payload包含token需要传达的信息,进行base64编码后为  eyJ1c2VySWQiOjAyMSwiYm9vayI6IuaciOS6ruS4juWFreS+v+WjqyJ9 组成jwt的第二部分 和 第一部分用. 分割开来。 因为base64编码后可以继续解码,所以载荷这里不推荐存储敏感信息,如密码信息等,负责容易解码泄露
第三部分:

加密后的信息,对前两部分的组合 eyJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ==.eyJ1c2VySWQiOjAyMSwiYm9vayI6IuaciOS6ruS4juWFreS+v+WjqyJ9

使用头部定义的hs256加密,该加密算法是对称加密,服务器保存密钥key,这里也可以使用rs256非对称加密。加密后的结果为TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ ,该部分与起那两部分用.分割开 。

所以最后的tokeneyJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ==.eyJ1c2VySWQiOjAyMSwiYm9vayI6IuaciOS6ruS4juWFreS+v+WjqyJ9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

jwt验证过程

 前面提到 token可以在用户登录认证成功后,随response信息返回前端。前端请求是放在http请求头部既可。
服务器在收到前端请求时候 ,对token验证 ,只需要一个密钥key即可,对头部和负载使用key进行加密,加密结果如果和签名signature一致,则说明token有效。

  这里假设token被恶意篡改了,比如更改了载荷payload里的一个value,然后base64编码后又被放置在新的token中。服务器收到该更改后的token,使用密钥key进行token的前两个部分进行加密校验,会发现加密后的结果和被更改后的token上的签名不一致,就能发现token已经被篡改,这时响应返回401既可。

         jwt实战应用:

1、引入依赖

   <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>RELEASE</version>
        </dependency>

 2、处理签名获取 token  ,这里编写一个工具类 JwtTokenUtil

添加配置文件 bootstrap-dev.yml:

jwt:
  secret: lile #密钥
  expiration: 100000 #过期时间
package utils;

import domain.UserInfo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName JwtTokenUtil
 * @Description 生成校验token
 * @Author lile
 * @Date 2020/2/15 14:27
 * @Version 1.0
 */
@Data
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtTokenUtil {

    private String secret;

    // 过期时间 毫秒
    private Long expiration;


    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + expiration);
        return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 生成令牌
     *
     * @param userDetails 用户
     * @return 令牌
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>(2);
        claims.put(Claims.SUBJECT, userDetails.getUsername());
        claims.put(Claims.ISSUED_AT, new Date());
        return generateToken(claims);
    }

    /**
     * 使用电话号生成令牌
     * @param phone
     * @return 令牌
     */
    public String generateTokenByUsername(String phone){
        Map<String, Object> claims = new HashMap<>(2);
        claims.put(Claims.SUBJECT, phone);
        claims.put(Claims.ISSUED_AT, new Date());
        return generateToken(claims);
    }

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

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    public Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return true;
        }
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put(Claims.ISSUED_AT, new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 验证令牌
     *
     * @param token       令牌
     * @param userDetails 用户
     * @return 是否有效
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        UserInfo user = (UserInfo) userDetails;
        String username = getUsernameFromToken(token);
        return (username.equals(user.getUsername()) && !isTokenExpired(token));
    }
}

在登录接口的中,如果登录成功,则在响应中添加token.


	@ApiOperation(value = "用户登录")
	@ApiResponses(
			{@ApiResponse(code=200,message="成功",response = DynamicResponse.class)}
	)
	@PostMapping
	@CrossOrigin
	@ResponseBody
	public DynamicResponse<User>  login (@RequestBody LoginRequest loginRequest){

		return DynamicResponse.of((()->{
			User user = userService.findUserByphone(loginRequest.getPhone());
			Checker.checkNoNull(user,ErrorCode.NOT_USER.throwSupplier("用户不存在"));
			Checker.checkTrue(PasswordUtil.sha256(user.getSalt(),loginRequest.getPwd()).equals(user.getPassword()),
					ErrorCode.LOGIN_FAILURE.throwSupplier("密码不正确"));
			// todo 处理token 封装res
			String token = jwtTokenUtil.generateTokenByUsername(loginRequest.getPhone());

			return  user;
		}));

	}


   3、增加注解  passtoken是绕过token验证; 另一个是必须要登录后才可以执行

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
/*
 * @Author leli
        * @Description //跳过token验证 * @Date 13:22 2020/2/10
 * @Param
        * @return
        **/
public @interface PassToken {
    boolean required() default  true;
}





@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
/*
 * @Author leli
        * @Description //需要登录才可以操作 * @Date 13:23 2020/2/10
 * @Param
        * @return
        **/
@interface UserLoginToken {
    boolean required() default  true;
}


     4、编写过滤器

package com.lile.handler;

import com.lile.common.mybits.model.User;
import com.lile.service.UserService;
import exceptions.UncheckedException;
import io.jsonwebtoken.JwtException;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import utils.ErrorCode;
import utils.JwtTokenUtil;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * @ClassName AuthenticationInterceptor
 * @Description TODO
 * @Author lile
 * @Date 2020/2/10 13:48
 * @Version 1.0
 */
public class AuthenticationInterceptor  implements HandlerInterceptor {
    @Resource
    private UserService userService;
    @Resource
    private JwtTokenUtil jwtTokenUtil;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
        // 如果不是映射到方法直接通过
        if(!(object instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod=(HandlerMethod)object;
        Method method=handlerMethod.getMethod();
        //检查是否有passtoken注释,有则跳过认证
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }
        //检查有没有需要用户权限的注解
        if (method.isAnnotationPresent(UserLoginToken.class)) {
            UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
            if (userLoginToken.required()) {
                // 执行认证
                if (token == null) {
                    throw new UncheckedException(ErrorCode.LOGIN_FAILURE,"无token,请重新登录");
                }
                // 获取 token 中的 phone ,phone唯一
                String phone;
                try {
                    phone = jwtTokenUtil.getUsernameFromToken(token);
                } catch (JwtException j) {
                    throw new UncheckedException(ErrorCode.PERMISSION_DENIED,"token错误,权限不足");
                }
                User user = userService.getUserById(Integer.parseInt(phone));
                if (user == null) {
                    throw new UncheckedException(ErrorCode.PERMISSION_DENIED,"用户不存在,重新登录");

                }
                // 验证 token
                if(!jwtTokenUtil.isTokenExpired(token)&& jwtTokenUtil.generateTokenByUsername(phone).equals(token)){
                    throw new UncheckedException(ErrorCode.PERMISSION_DENIED,"token过期或不正确");
                }

                return true;
            }
        }
        return true;

    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }
}


        
        5、配置拦截器

package com.lile.config;

import com.lile.handler.AuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class CorsConfig {
    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 配置所有请求
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
        return new AuthenticationInterceptor();
    }
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry
                        .addMapping("/**")
                        .allowedHeaders("*")
                        .allowedMethods("*")
                        .allowedOrigins("*");
            }
             // 使用上面定义的拦截器 
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                registry.addInterceptor(authenticationInterceptor())
                        .addPathPatterns("/**");
            }
        };
    }
}


6、 应用 

在需要登录才能执行的方法前面使用@UserloginToken  

如下:

@RestController
public class HellowordController {

	@UserLoginToken
	@GetMapping("/hello")
	public String index(){
		return "hellow rod";
	}
	
}

在swagger中试一下 请求该接口:

 

该接口会验证该request中是否包含正确的token 。

 

上面jwt是基于WebMvcConfigurer自定义过滤器进行安全控制,后面将介绍jwt和 spring security组合使用。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值