什么是单点登录?
单点登录(Single Sign On),简称为 SSO,在分布式架构项目中,只需要在一个节点进行登录验证,就能够在其它的所有相关节点实现访问。
为什么要使用单点登录?
原来登录的过程:
1)用户输入账号密码
2)提交到后台验证,成功后将用户存在Session中
3)需要进行登录状态判断时,判断Session中是否存在该对象
分布式系统的多个相关的应用系统,都需要分别进行登录,非常繁琐。
存在的问题:
分布式系统有N个服务器,每个服务器有自己的Session,无法登录一次,所有服务器能判断用户登录状态
单点登录的实现方案
SSO有哪些常见的解决方案
1)使用Redis实现Session共享
有状态的登录,需要在服务器中保存用户的数据;REST架构推荐使用无状态通信,不在服务器端保存用户状态,服务器压力更小,成本更低,扩展更加容器。
2)使用Token机制实现
将用户的状态保存到客户端的cookie中,每次请求服务器时,都会携带用户信息,服务器对用户信息进行解析和判断,来进行登录鉴权。
1)用户输入账号密码,通过网关,进入验证服务
2)验证服务进行登录验证
3)验证成功后,将用户信息保存到token字符串,将token写入cookie
4)cookie被保存到用户浏览器中
5)用户再访问微服务时,经过网关,网关对token进行解析
6)解析成功,允许访问其他微服务
7)解析失败,不允许访问
这种方式是无状态登录,服务器不保存用户状态,状态保存到客户端,信息存在安全性问题,需要加密。
JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
官网:https://jwt.io
可以在官网测试保存信息到JWT中,可以看到JWT分为三个部分:
- header 头部,包含声明类型和加密算法
- payload 负载,就是有效数据,一般是用户信息
- signature 签名,数据的认证信息
JWT的交互流程
- 用户登录,发送账号密码
- 服务的认证,通过后根据secret生成token
- 将生成的token返回给浏览器
- 用户每次请求携带token
- 服务端利用公钥解读jwt签名,判断签名有效后,从Payload中获取用户信息
- 处理请求,返回响应结果
单点登录的实现
- JWT+Gateway方案
- OAuth2方案
- 共享session
实现步骤:
- 创建用户数据库,用户表
- 创建用户服务,添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>com.blb</groupId>
<version>0.0.1-SNAPSHOT</version>
<artifactId>common_api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 注册到Eureka上:在启动类上加注解
@EnableDiscoveryClient
- 配置数据源和mybatis-plus
- 编写entity、mapper、service
- 编写UserDetailsService实现类
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//按用户名查询
User user = userMapper.selectOne(new QueryWrapper<User>().lambda().eq(User::getUsername, s));
if(user==null){
throw new UsernameNotFoundException("用户名不存在");
}
//返回正确的用户信息
return new org.springframework.security.core.userdetails.User(s,user.getPassword(),
AuthorityUtils.commaSeparatedStringToAuthorityList(""));
}
}
- 编写Security配置,登录成功的处理器
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 用来记录账号,密码,角色信息。
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置自定义登录逻辑
auth.userDetailsService(userDetailsService);
}
/**
* 也就是对角色的权限——所能访问的路径做出限制
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置放行url
http.authorizeRequests()
.antMatchers("/login","/logout","/swagger-ui.html","/swagger-recources/**","/webjars/**","/api-docs").permitAll()
.anyRequest().authenticated() //配置其它url要验证
.and()
.formLogin() //配置登录相关
.successHandler(new LoginSuccessHandler()) //配置登录成功的处理器
.failureHandler((req,resp,auth) -> { //配置登录失败的处理器
ResponseResult.write(resp, ResponseResult.error(ResponseStatus.LOGIN_ERROR));
})
.and()
.exceptionHandling()
.authenticationEntryPoint((req,resp,auth) ->{ //配置拦截未登录请求的处理
ResponseResult.write(resp,ResponseResult.error(ResponseStatus.AUTHENTICATE_ERROR));
})
.and()
.logout()
.logoutSuccessHandler((req,resp,auth) ->{ //配置登出处理器
ResponseResult.write(resp,ResponseResult.ok("注销成功"));
})
.clearAuthentication(true) //清除验证缓存
.and()
.csrf()//跨站攻击手段
.disable() //关闭csrf保护
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS); //不使用session
}
}
- 在网关配置用户的路由
- id: user-service-route
uri: lb://user-service
predicates:
- Path=/login,/logout,/user/**
- 开发放行接口的白名单
user:
white-list: # 自定义白名单
- /login
- /logout
@Data
@Configuration
@ConfigurationProperties(prefix = "user")
public class WhiteListConfig {
private List<String> whiteList;
}
- 编写网关过滤器
/**
* 用户验证过滤器
*/
@Slf4j
@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {
@Autowired
private WhiteListConfig whiteListConfig;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获得请求和响应对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//对白名单中的地址放行
List<String> whiteList = whiteListConfig.getWhiteList();
for(String str : whiteList){
if(request.getURI().getPath().contains(str)){
log.info("白名单,放行{}",request.getURI().getPath());
return chain.filter(exchange);
}
}
//获得请求头中Authorization token信息
String token = request.getHeaders().getFirst("Authorization");
try{
//解析token
String username = JwtUtil.getUsernameFromToken(token, RsaUtil.publicKey);
log.info("{}解析成功,放行{}",username,request.getURI().getPath());
return chain.filter(exchange);
}catch (Exception ex){
log.error("token解析失败",ex);
//返回验证失败的响应信息
response.setStatusCode(HttpStatus.UNAUTHORIZED);
DataBuffer wrap = response.bufferFactory().wrap("验证错误,需要登录".getBytes());
return response.writeWith(Mono.just(wrap));
}
}
@Override
public int getOrder() {
return 0;
}
}