SSO单点登录

1、单一服务器模式

  1. 用户向服务器发送用户名和密码。
  2. 验证服务器后,相关数据(如用户角色,登录时间等)将保存在当前会话中。
  3. 服务器向用户返回session_id,session信息都会写入到用户的Cookie。
  4. 用户的每个后续请求都将通过在Cookie中取出session_id传给服务器。
  5. 服务器收到session_id并对比之前保存的数据,确认用户的身份。

SSO单点登录_第1张图片

2、SSO(single sign on)模式

随着业务扩大,业务分开部署,存储在session中时无法在另一台服务器中判断登录。

2.1、session广播

  • 将同服务器的session复制到其他服务器中

SSO单点登录_第2张图片

缺点:数据冗余,增加服务器的负担

2.2、cookie+redis

  1. 登录之后数据存储在两个地方;Redis中Key生成用户的唯一id,value值保存用户信息,浏览器将Redis生成的唯一id保存在cookie中。
  2. 访问其他模块,发送请求时携带cookie,取出cookie中的id对redis操作判断是否登录。

SSO单点登录_第3张图片

优点: 用户身份信息独立管理,更好的分布式管理; 可以自己扩展安全策略

缺点:认证服务器访问压力较大

  1. Spring-session实现全局session保存
  • pom.xml
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
    <groupId>org.springframework.sessiongroupId>
    <artifactId>spring-session-data-redisartifactId>
dependency>
  • application.properties
server.port=9000
spring.application.name=spring-session

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0
  • 开启spring-session
@EnableRedisHttpSession
@SpringBootApplication
public class SsoApplication {
     
    public static void main(String[] args) {
     
        SpringApplication.run(SsoApplication.class, args);
    }
}
  • Controller
@GetMapping("put")
public String putSession(HttpSession session){
     
    session.setAttribute("user","登录信息");
    return "ok";
}

@GetMapping("get")
public String getSession(HttpSession session){
     
    Object user = session.getAttribute("user");
    return "ok"+user;
}
  • redis
#keys *
 1)  "spring:session:expirations:1602219480000"
 2)  "spring:session:sessions:expires:d131e843-75f6-4c9c-97ab-a0a884a95a28"
 3)  "spring:session:sessions:d131e843-75f6-4c9c-97ab-a0a884a95a28"
  • 自定义序列化
@Configuration
public class RedisConfig {
     
    @Bean
    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory factory){
     
        RedisTemplate<Object,Object> redisTemplate =new RedisTemplate<>(); 
        redisTemplate.setConnectionFactory(factory);
        //Jackson序列化value
        Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jsonRedisSerializer.setObjectMapper(mapper);

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jsonRedisSerializer);
        redisTemplate.setValueSerializer(jsonRedisSerializer);

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory){
     
        StringRedisTemplate redisTemplate =  new StringRedisTemplate();
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
}

  • 原理
protected void doFilterInternal(HttpServletRequest request, 
                                HttpServletResponse response, 
                                FilterChain filterChain)throws ServletException, IOException {
     
    
    request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

    SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);//请求包装
    SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,response);//响应包装
    try {
     
        filterChain.doFilter(wrappedRequest, wrappedResponse);//包装过的请求响应通过过滤器
    }
    finally {
     
        wrappedRequest.commitSession();
    }
}

2.3、token

  1. 登录之后,根据用户信息按照一定的规则生成字符串发送给前端。
  2. 访问其他模块时,使用cookie携带token或地址栏参数携带token发送请求,后端收到token值进行解析判断登录

优点:无状态: token无状态,session有状态的;基于标准化: 可以采用标准化的 JSON Web Token (JWT)

缺点:占用带宽;无法在服务器端销毁

SSO单点登录_第4张图片

  1. JWT保存登录信息
  • JWT头:描述JWT元数据的JSON对象
  • alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);
  • typ属性表示令牌的类型,JWT令牌统一写为JWT。
  • 最后,使用Base64 URL算法将JSON对象转换为字符串保存
{
     
  "alg": "HS256",
  "typ": "JWT"
}
  • JWT主体:有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。

请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信

息,以防止信息泄露。JSON对象也使用Base64 URL算法转换为字符串保存。

默认提供字段:
	#iss:发行人
	#exp:到期时间
	#sub:主题
	#aud:用户
	#nbf:在此之前不可用
	#iat:发布时间
	#jti:JWT ID用于标识该JWT
自定义字段:
{
     
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
}
  • JWT签名:签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
  • 首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。
  • 然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。
  • JWT问题和趋势
  1. JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。

  2. 生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库

  3. 存储在客户端,不占用服务端的内存资源

  4. JWT默认不加密,但可以加密。生成原始令牌后,可以再次对其进行加密。

  5. 当JWT未加密时,一些私密数据无法通过JWT传输。

  6. JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,

    一旦JWT签发,在有效期内将会一直有效。

  7. JWT本身包含认证信息,token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。

  8. 为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。

  • JWT工具类
public class JwtUtils {
     

    //过期时间
    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    //服务器密钥
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

    //生成token
    public static String getJwtToken(String id, String nickname){
     
        String JwtToken = Jwts.builder()
            //头信息
            .setHeaderParam("typ", "JWT")
            .setHeaderParam("alg", "HS256")
            //默认主体
            .setSubject("user")
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
            //自定义主体
            .claim("id", id)
            .claim("nickname", nickname)
            //签名hash
            .signWith(SignatureAlgorithm.HS256, APP_SECRET)
            .compact();
        return JwtToken;
    }
    // 判断token是否存在与有效   
    public static boolean checkToken(String jwtToken) {
     
        if(StringUtils.isEmpty(jwtToken)) return false;
        try {
     
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
        return true;
    }
    // 判断token是否存在与有效 
    public static boolean checkToken(HttpServletRequest request) {
     
        try {
     
            String jwtToken = request.getHeader("token");
            if(StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
     
            e.printStackTrace();
            return false;
        }
        return true;
    }
    //根据token获取会员id   
    public static String getIdByReq(HttpServletRequest request) {
     
        String jwtToken = request.getHeader("token");
        if(StringUtils.isEmpty(jwtToken)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).
            parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("id");
    }
    //根据token获取会员id
    public static String getIdByJwt(String token) {
     
        if(StringUtils.isEmpty(token)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("id");
    }
}
  • controller
@GetMapping("token/put")
public String putSession(){
     
    return JwtUtils.getJwtToken("1", "wdd");
}
//eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
//eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNjAyMjIyNzQ0LCJleHAiOjE2MDIzMDkxNDQsImlkIjoiMSIsIm5pY2tuYW1lIjoid2RkIn0.
//LJyXdXttVyd1xI91QetkcWKd3WhuNiaJs3aLWy8RqcY


@GetMapping("token/get")
public String getSession(@RequestParam("token")String token){
     
    return JwtUtils.getIdByJwt(token);
}

你可能感兴趣的:(jwt,cookie,session,java,sso)