Spring Secuirty Oauth2实现SSO
创建客户端:oauth2-sso-client-demo
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
修改application.properties
server.port=8081
#防止Cookie冲突,冲突会导致登录验证不通过
server.servlet.session.cookie.name=OAUTH2-CLIENT-SESSIONID01
#授权服务器地址
oauth2-server-url: http://localhost:8080
#与授权服务器对应的配置
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=123123
security.oauth2.client.user-authorization-uri=${oauth2-serverurl}/oauth/authorize
security.oauth2.client.access-token-uri=${oauth2-server-url}/oauth/token
security.oauth2.resource.jwt.key-uri=${oauth2-server-url}/oauth/token_key
在启动类上添加@EnableOAuth2Sso注解来启用单点登录功能
@EnableOAuth2Sso单点登录的原理简单来说就是:标注有@EnableOAuth2Sso的OAuth2 Client应用在通过某种OAuth2授权流程获取访问令牌后(一般是授权码流程),通过访问令牌访问userDetails用户明细这个受保护资源服务,获取用户信息后,将用户信息转换为Spring Security上下文中的认证后凭证Authentication,从而完成标注有@EnableOAuth2Sso的OAuth2 Client应用自身的登录认证的过程。整个过程是基于OAuth2的SSO单点登录
@SpringBootApplication
@EnableOAuth2Sso
public class Oauth2SsoClientDemoApplication {
public static void main(String[] args) {
SpringApplication.run(Oauth2SsoClientDemoApplication.class, args);
}
}
添加接口用于获取当前登录用户信息
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication) {
return authentication;
}
}
授权服务器:oauth2-jwt-demo
修改授权服务器中的AuthorizationServerConfig类,将绑定的跳转路径为http://localhost:8081/login,并添加获取秘钥时的身份认证
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//允许表单认证
security.allowFormAuthenticationForClients()
// 获取密钥需要身份认证,使用单点登录时必须配置
.tokenKeyAccess("isAuthenticated()");
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//配置client_id
.withClient("client")
//配置client-secret
.secret(passwordEncoder.encode("123123"))
//配置访问token的有效期
.accessTokenValiditySeconds(3600)
//配置刷新token的有效期
.refreshTokenValiditySeconds(864000)
//配置redirect_uri,用于授权成功后跳转
.redirectUris("http://localhost:8081/login")
//自动授权配置
.autoApprove(true)
//配置申请的权限范围
.scopes("all")
/**
* 配置grant_type,表示授权类型
* authorization_code: 授权码
* password: 密码
* client_credentials: 客户端
* refresh_token: 更新令牌
*/
.authorizedGrantTypes("authorization_code","password","refresh_token");
}
测试
启动授权服务和客户端服务;
访问客户端需要授权的接口http://localhost:8081/user/getCurrentUser 会跳转到授权服务的登录界面;
授权后会跳转到原来需要权限的接口地址,展示登录用户信息
模拟两个客户端8081,8082
修改application.properties配置
server.port=8082
#防止Cookie冲突,冲突会导致登录验证不通过
server.servlet.session.cookie.name=OAUTH2-CLIENT-SESSIONID${server.port}
修改授权服务器配置,配置多个跳转路径
//配置redirect_uri,用于授权成功后跳转
.redirectUris("http://localhost:8081/login",
"http://localhost:8082/login")
8081登录成功之后,8082无需再次登录就可以访问http://localhost:8082/user/getCurrentUser
Spring Cloud中如何实现SSO
网关整合 OAuth2.0 有两种思路,一种是授权服务器生成令牌, 所有请求统一在网关层验证,判断权限等操作;另一种是由各资源服务处理,网关只做请求转发。 比较常用的是第一种,把API网关作为OAuth2.0的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当前登录用户信息给微服务,这样下游微服务就不需要关心令牌格式解析以及OAuth2.0相关机制了。
网关在认证授权体系里主要负责两件事:
- 作为OAuth2.0的资源服务器角色,实现接入方权限拦截。
- 令牌解析并转发当前登录用户信息(明文token)给微服务
微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事:
- 用户授权拦截(看当前用户是否有权访问该资源)
- 将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)
核心代码,网关自定义全局过滤器进行身份认证
@Component
@Order(0)
public class AuthenticationFilter implements GlobalFilter, InitializingBean {
@Autowired
private RestTemplate restTemplate;
private static Set<String> shouldSkipUrl = new LinkedHashSet<>();
@Override
public void afterPropertiesSet() throws Exception {
// 不拦截认证的请求
shouldSkipUrl.add("/oauth/token");
shouldSkipUrl.add("/oauth/check_token");
shouldSkipUrl.add("/user/getCurrentUser");
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestPath = exchange.getRequest().getURI().getPath();
//不需要认证的url
if(shouldSkip(requestPath)) {
return chain.filter(exchange);
}
//获取请求头
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
//请求头为空
if(StringUtils.isEmpty(authHeader)) {
throw new RuntimeException("请求头为空");
}
TokenInfo tokenInfo=null;
try {
//获取token信息
tokenInfo = getTokenInfo(authHeader);
}catch (Exception e) {
throw new RuntimeException("校验令牌异常");
}
exchange.getAttributes().put("tokenInfo",tokenInfo);
return chain.filter(exchange);
}
private boolean shouldSkip(String reqPath) {
for(String skipPath:shouldSkipUrl) {
if(reqPath.contains(skipPath)) {
return true;
}
}
return false;
}
private TokenInfo getTokenInfo(String authHeader) {
// 获取token的值
String token = StringUtils.substringAfter(authHeader, "bearer ");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setBasicAuth(MDA.clientId, MDA.clientSecret);
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("token", token);
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers);
ResponseEntity<TokenInfo> response = restTemplate.exchange(MDA.checkTokenUrl, HttpMethod.POST, entity, TokenInfo.class);
return response.getBody();
}
}