风止雨歇

单体应用的session & 分布式Session & JWT

一、单体应用的session

1、查询用户名和密码;
2、使用 Security 的 PasswordEncoder 去校验用户名和密码;
3、将 用户信息放入 session 中;
(1)定义一个 BaseController,获取请求和响应;
public class BaseController {

    public HttpServletRequest getRequest(){
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }

    public HttpServletResponse getResponse(){
        return ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getResponse();
    }

    public HttpSession getHttpSession(){
        return getRequest().getSession();
    }
}

(2)放入用户信息到 session 中;

getHttpSession().setAttribute("member", member);

4、只有登录后,接口才可以去访问;

自定义拦截器,拦截其他接口的登录;实现 HandlerInterceptor 接口,重写 preHandle 方法。
@Slf4j
public class AuthInterceptorHandler implements HandlerInterceptor {
    public final static String GLOBAL_JWT_USER_INFO="jwttoken:usermember:info";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("进入前置拦截器");
        if(!ObjectUtils.isEmpty(request.getSession().getAttribute("member"))){
            return true;
        }
        print(response,"您没有权限访问!请先登录.");
        return false;
    }

    protected void print(HttpServletResponse response,String message) throws Exception{
        /**
         * 设置响应头
         */
        response.setHeader("Content-Type","application/json");
        response.setCharacterEncoding("UTF-8");
        String result = new ObjectMapper().writeValueAsString(CommonResult.forbidden(message));
        response.getWriter().print(result);
    }
}

6、将拦截器加入到配置中;

(1)配置文件的配置;

#登录拦截验证
member:
  auth:
    shouldSkipUrls:
     - /sso/**
     - /home/**

(2)加载配置文件中的配置到 Set 集合中;

@Data
@ConfigurationProperties(prefix = "member.auth")
public class NoAuthUrlProperties {
    private LinkedHashSet<String> shouldSkipUrls;
}

(3)拦截器的配置;

@EnableConfigurationProperties(NoAuthUrlProperties.class)
@Configuration
public class IntercepterConfiguration implements WebMvcConfigurer {

    @Autowired
    private NoAuthUrlProperties noAuthUrlProperties;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册拦截器
        registry.addInterceptor(authInterceptorHandler())
                .addPathPatterns("/**")
                .excludePathPatterns(new ArrayList<>(noAuthUrlProperties.getShouldSkipUrls()));
    }

    @Bean
    public AuthInterceptorHandler authInterceptorHandler(){
        return new AuthInterceptorHandler();
    }
}

单体应用部署多个,上面的代码就会出现问题,每次去访问的不同实例的时候,就需要再重新登录;、

可以使用spring session 解决以上问题。

 

二、使用 spring session 实现分布式Session

1、引入 spring session 的 jar 包;

<dependency>
      <groupId>org.springframework.session</groupId>
      <artifactId>spring-session-data-redis</artifactId>
</dependency>

2、配置文件中配置存储类型;可以使用 redis、mongodb、mysql;

spring:
  session:
    store-type: redis

3、开启spring session的配置;(以下session的超时时间设置为 3600s)

@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
public class RedisHttpSessionConfiguration {
    /**
     * 引入分布式会话体系,会话内容存储在Redis当中,原理请阅读源码
     */
}

 

 三、Spring Session的简介

Spring Session 使用过滤器 Filter 实现了session 的共享;

(1)Spring Session 内部不是每次都去从 Redis 中获取Session,它的本地缓存中也会保留一份 session;本地缓存中的 session 过期找不到的时候才会去连接 Redis 查询;

(2)本地缓存 session 的过期时间是根据配置文件中的 spring.session.redis.cleanup-cron 的表达式配置来处理的;

(3)不是每次去调用 getSession() 或 setAttribute() 方法的时候都会将 Redis 中的超时时间重置,是在过滤器调用链走完之后,再将 Redis 中的超时时间重置(finally 语句中的 warppedRequest.commitSession() ),保证每次请求只将 Redis 中的过期时间重置一次;

 

@EnableRedisHttpSession 注解通过Import,引入了RedisHttpSessionConfiguration配置类。该配置类通过@Bean注解,向Spring容器中注册了一个SessionRepositoryFilterSessionRepositoryFilter的依赖关系:SessionRepositoryFilter --> SessionRepository --> RedisTemplate --> RedisConnectionFactory)。

Spring Session源码参考

(1)spring-session(一)揭秘: https://www.cnblogs.com/lxyit/p/9672097.html

(2)利用spring session解决共享Session问题: https://blog.csdn.net/patrickyoung6625/article/details/45694157

 

四、JWT

1、JWT介绍

  全称JSON Web Token,用户会话信息存储在客户端浏览器,它定义了一种紧凑且自包含的方式,用于在各方之间以JSON对象进行安全传输信息。这些信息可以通过对称/非对称方式进行签名,防止信息被篡改。
 

2、JWT数据格式(JWT = Header.Payload.Signature)

(1)Header:头部
{
    alg: "HS256",
    typ: "JWT"
}
  • alg属性表示签名的算法,默认算法为HS256,可以自行别的算法。
  • typ属性表示这个令牌的类型,JWT令牌就为JWT。

Header = Base64(上方json数据)

 

(2)Payload:载荷

存放用户的信息,如创建时间、过期时间;例如:

{    
    "userid":"test",
    "created":1489079981393,
    "exp":1489684781
}

 Payload = Base64(data)  //可以被反编码,所以不要放入敏感信息

 

(3)Signature:签名

 使用头部中存储的签名算法去签名;

Signature = HMACSHA256(base64UrlEncode(header)  +  "."  + base64UrlEncode(payload), secret)

secret为加密的密钥,密钥存在服务端;

3、JWT身份认证流程

(1)用户提供用户名和密码登录;
(2)服务器校验用户是否正确,如正确,就返回token给客户端,此token可以包含用户信息;
(3)客户端存储token,可以保存在cookie或者local storage;
(4)客户端以后请求时,都要带上这个token,一般放在请求头中;
(5)服务器判断是否存在token,并且解码后就可以知道是哪个用户;
(6)服务器这样就可以返回该用户的相关信息了;

 

 

 

4、JWT的使用

(1)引入JWT的工具包
<!-- json web token 工具 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
</dependency>
(2)定义JWT的生成和解析工具类

 a. 配置文件

#jwt config
jwt:
  tokenHeader: Authorization #JWT存储的请求头
  secret: mall-member-secret #JWT加解密使用的密钥
  expiration: 604800 #JWT的超期限时间(60*60*24)
  tokenHead: Bearer #JWT负载中拿到开头

b. 读取配置文件

@Data
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtProperties {
    private String tokenHeader;
    private String secret;
    private Long expiration;
    private String tokenHead;
}

c. JWT的生成 和 解析

public class JwtKit {
    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 创建jwtToken
     * @param member
     * @return
     */
    public String generateJwtToken(UmsMember member){
        Map<String,Object> claims = new HashMap<>();

        claims.put("sub",member.getUsername());
        claims.put("createdate",new Date());
        claims.put("id",member.getId());
        claims.put("memberLevelId",member.getMemberLevelId());

        return Jwts.builder()
                .addClaims(claims) 
                .setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration()*1000))
                .signWith(SignatureAlgorithm.HS256,jwtProperties.getSecret())
                .compact();
    }
    
    /**
     * 解析jwt
     * @param jwtToken
     * @return
     * @throws BusinessException
     */
    public Claims parseJwtToken(String jwtToken) throws BusinessException {
        Claims claims = null;
        try {
            claims=Jwts.parser()
                    .setSigningKey(jwtProperties.getSecret())
                    .parseClaimsJws(jwtToken)
                    .getBody();
        } catch (ExpiredJwtException e) {
            throw new BusinessException("JWT验证失败:token已经过期");
        } catch (UnsupportedJwtException e) {
            throw new BusinessException("JWT验证失败:token格式不支持");
        } catch (MalformedJwtException e) {
            throw new BusinessException("JWT验证失败:无效的token");
        } catch (SignatureException e) {
            throw new BusinessException("JWT验证失败:无效的token");
        } catch (IllegalArgumentException e) {
            throw new BusinessException("JWT验证失败:无效的token");
        }
        return claims;
    }

}

d. 将工具类注入到容器中

@Configuration
public class SecurityConfiguration {
    @Bean
    public JwtKit jwtKit(){
        return new JwtKit();
    }
}

e. 自定义拦截器,解析对应的JWT token;

@Slf4j
public class AuthInterceptorHandler implements HandlerInterceptor {
    @Autowired
    private JwtKit jwtKit;

    @Autowired
    private JwtProperties jwtProperties;

    public final static String GLOBAL_JWT_USER_INFO="jwttoken:usermember:info";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("进入前置拦截器");
        String message = null;
        String authorization = request.getHeader(jwtProperties.getTokenHeader());
        log.info("authorization:"+authorization);
        //校验token

        if(!StringUtils.isEmpty(authorization)
                && authorization.startsWith(jwtProperties.getTokenHead())){
            String authToken = authorization.substring(jwtProperties.getTokenHead().length());
            //解析jwt-token
            Claims claims = null;
            try {
                claims = jwtKit.parseJwtToken(authToken);
                if(claims != null){
                    request.setAttribute(GLOBAL_JWT_USER_INFO,claims);
                    return true;
                }
            } catch (BusinessException e) {
                log.error(message = (e.getMessage()+":"+authToken));
            }
        }
        print(response,"您没有权限访问!请先登录.");
        return false;
    }

    protected void print(HttpServletResponse response,String message) throws Exception{
        //设置响应头
        response.setHeader("Content-Type","application/json");
        response.setCharacterEncoding("UTF-8");
        String result = new ObjectMapper().writeValueAsString(CommonResult.forbidden(message));
        response.getWriter().print(result);
    }
}

f.  将自定义的拦截器加入到配置中

@Configuration
public class IntercepterConfiguration implements WebMvcConfigurer {
    @Bean
    public AuthInterceptorHandler authInterceptorHandler(){
        return new AuthInterceptorHandler();
    }
}

 

五、session 和 JWT 的比较

JWT

(1)用户信息存储在客户端(storage,cookie);

(2)JWT泄露之后,只要没有过期,都可以被使用;即使修改完密码后,泄露出去的JWT仍然可以使用;

(3)JWT的安全性低于Session,JWT的使用的时候先要进行数据脱敏的处理;

Session(有状态)

(1)只在 cookie 中存储一个 jSessionId,用户信息存储在服务端,根据 jSessionId 去查询对应的用户信息;

(2)安全性较高;

 

Session 和 JWT 可以结合一起使用;

 

 

posted on 2021-12-06 23:08  风止雨歇  阅读(386)  评论(1编辑  收藏  举报

导航