什么是单点登录
单点登录(Single Sign On),简称为 SSO,在分布式架构项目中,只需要在一个节点进行登录验证,就能够在其它的所有相关节点实现访问。
实现方案:
-
JWT+Gateway方案
-
OAuth2方案
-
共享session
这里我们实现一下第一种方案:
1.用户发送请求给网关,在网关当中设置的有登录白名单,白名单放行
2.放行后去访问用户服务,进行密码校验,如果密码正确进入登录成功处理器,生成token
3.返回token给前台,保存到localstorage中
4.然后再次发送请求并携带token进入网关,在网关对token进行验证,验证成功之后放心,失败返回错误信息给前台
具体实现:
网关配置:
spring:
cloud:
gateway:
routes: # 路由
- id: order-service-route
uri: lb://order-service # 服务名称
predicates:
# 断言
- Path=/order/**,/orders/** # 匹配路径
- id: product-service-route
uri: lb://product-service
predicates:
- Path=/product/**,/products/**
- id: user-service-route
uri: lb://user-service
predicates:
- Path=/user/**,/login/**,/logout/**
globalcors:
cors-configurations:
# 跨域配置
'[/**]':
# 匹配所有路径
allowed-origins:
# 允许的域名
- "http://localhost:8080"
allowed-headers: "*"
# 允许的请求头
allowed-methods: "*"
# 允许的方法
allow-credentials: true # 是否携带cookie
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
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 {
String username = JwtUtil.getUsernameFromToken(token, RsaUtil.publicKey);
log.info("{}解析成功,放行{}",username,request.getURI().getPath());
return chain.filter(exchange);
}catch (Exception e){
log.error("token解析失败",e);
DataBuffer wrap = response.bufferFactory().wrap("验证错误,需要登录".getBytes());
return response.writeWith(Mono.just(wrap));
}
}
@Override
public int getOrder() {
return 0;
}
}
登录成功处理器:
具体要看前台要返回的是user对象还是username进行适当修改
@Slf4j
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private UserMapper userMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//获得用户名
User user = (User) authentication.getPrincipal();
//将用户名生成jwt token
String token = JwtUtil.generateToken(user.getUsername(), RsaUtil.privateKey, JwtUtil.EXPIRE_MINUTES);
//将token 发送给前端
com.blb.common.entity.User user1 = userMapper.selectOne(new QueryWrapper<com.blb.common.entity.User>().lambda().eq(com.blb.common.entity.User::getUsername, user.getUsername()));
UserTokenVO userTokenVo = new UserTokenVO(user1,token);
ResponseResult.write(response, ResponseResult.ok(userTokenVo));
log.info("user:{} token:{}",user.getUsername() , token);
}
}
security配置:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置自定义登录逻辑
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置放行url
http.authorizeRequests()
.antMatchers("/swagger-ui.html","/swagger-resources/**","/webjars/**","/*/api-docs"
,"/login","/logout").permitAll()
.anyRequest().authenticated() //配置其它url要验证
.and()
.formLogin() //配置登录相关
.successHandler(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
}
}
user的vo对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserTokenVO {
private User user;
private String token;
}
在serviceImpl进行用户名验证:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
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(""));
}
}