authorization server && client && resource 使用2
oauth2 整合 jwt
authorization server && client && resource 使用1 中默认的示例就是使用的jwt 生成token(用于),当然这里和我们用户登录的token是有区别的
oauth2 server 和jwt
流程
我们授权服务器端支持授权码模式(用户需要登录)和客户端默认(和用户无关)
步骤
生成
1、OAuth2ClientAuthenticationFilter 对clientid 和clientsecret做一些验证,和jwt无关
2、然后不管是哪种模式在上一步验证通过后会进入OAuth2TokenEndpointFilter中再次进行验证,
ProviderManager#authenticate
OAuth2ClientCredentialsAuthenticationProvider#authenticate(这里认证成功后会生成token)
DelegatingOAuth2TokenGenerator#generate
JwtGenerator#generate
解析
BearerTokenAuthenticationFilter#doFilterInternal
ProviderManager#authenticate
JwtAuthenticationProvider#authenticate
JwtAuthenticationProvider#getJwt (这里使用了jwtDecoder)
JwtAuthenticationConverter#convert
配置生效
一样的,搞不明白,
官方oauth结合jwt示例
代码
@Configuration
public class RestConfig {
@Value("${jwt.public.key}")
RSAPublicKey key;
@Value("${jwt.private.key}")
RSAPrivateKey priv;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.csrf((csrf) -> csrf.ignoringAntMatchers("/token"))
.httpBasic(Customizer.withDefaults())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler())
);
// @formatter:on
return http.build();
}
@Bean
UserDetailsService users() {
// @formatter:off
return new InMemoryUserDetailsManager(
User.withUsername("user")
.password("{noop}password")
.authorities("app")
.build()
);
// @formatter:on
}
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.key).build();
}
@Bean
JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
}
@RestController
public class HelloController {
@GetMapping("/")
public String hello(Authentication authentication) {
return "Hello, " + authentication.getName() + "!";
}
}
@RestController
public class TokenController {
@Autowired
JwtEncoder encoder;
@PostMapping("/token")
public String token(Authentication authentication) {
Instant now = Instant.now();
long expiry = 36000L;
// @formatter:off
String scope = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(" "));
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plusSeconds(expiry))
.subject(authentication.getName())
.claim("scope", scope)
.build();
// @formatter:on
return this.encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}
测试
获取token, 访问需要权限验证的接口,只要带上认证的请求头,Basic base64( 用户名:密码)
获取token之后通过token访问其他接口
突然发现只要知道了你的jwt秘钥,然后就。。。。。
然后就是示例中还直接指定了异常处理,这个似乎就一个返回403和401的作用,具体看相应的实现
自定义JWT配置
数据样例
{
"sub": "sry",
"aud": "messaging-client",
"nbf": 1669812400,
"scope": [
"openid",
"profile",
"message.read",
"message.write"
],
"iss": "http://localhost:8080",
"exp": 1669812700,
"iat": 1669812400
}
配置类
更多配置方法查看 官方文档,感觉有点多,这儿也有一部分,官方文档2
配置项可以查看 OAuth2ResourceServerConfigurer.JwtConfigurer
配置类更适合扩展,如果没有特殊需求,全部在配置文件中进行配置更简单
公钥和私钥外置
官方提供其他读取配置文件的属性具体参考OAuth2ResourceServerProperties,这里是自定义的读取方式,需要结合自定义配置类
jwt:
private.key: classpath:app.key
public.key: classpath:app.pub
也可以生成jks文件
配置类
主要是解码器,编码器,openid(id_token)扩展,token内容扩展,token设置(包含失效时间)等相关配置,当然官网还有很多其他配置,
@Configuration
public class Oauth2JwtConfig {
@Value("${jwt.public.key}")
RSAPublicKey pubKey;
@Value("${jwt.private.key}")
RSAPrivateKey priKey;
/**
* 自定义校验
* */
@Bean
OAuth2TokenValidator<Jwt> audienceValidator() {
return new JwtClaimValidator<List<String>>(AUD, aud -> aud.contains("messaging"));
}
/**
* 向jwt token 的payload中添加自定义数据
* */
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer(OidcUserInfoService userInfoService) {
return (context) -> {
// 授权码登录这里可以获取到用户名称,客户端登录这里可以获取到客户端id
JwtClaimsSet.Builder claims1 = context.getClaims();
// 如果是access_token模式,添加对应的返回数据
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
context.getClaims().claims((claims) -> {
claims.put("claim-1", "value-1");
});
}
// 如果是id_token模式,这里添加对应的返回数据,一般
if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
OidcUserInfo userInfo = userInfoService.loadUser(
context.getPrincipal().getName());
context.getClaims().claims(claims ->
claims.putAll(userInfo.getClaims()));
}
};
}
@Bean
public JWKSource<SecurityContext> jwkSource(KeyPair keyPair) {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
// 解码的自定义处理器
jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
return jwtDecoder;
}
@Bean
JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
return new NimbusJwtEncoder(jwkSource);
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
KeyPair generateRsaKey() {
// 直接使用配置文件中的公私钥,具体可以使用java自带的
return new KeyPair(this.pubKey,this.priKey);
}
class UsernameSubClaimAdapter implements Converter<Map<String, Object>, Map<String, Object>> {
private final MappedJwtClaimSetConverter delegate =
MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());
public Map<String, Object> convert(Map<String, Object> claims) {
Map<String, Object> convertedClaims = this.delegate.convert(claims);
String username = (String) convertedClaims.get("user_name");
convertedClaims.put("sub1", "你好呀");
return convertedClaims;
}
}
}
扩展id_token的内容,增加userinfo接口的返回数据
@Service
public class OidcUserInfoService {
private final UserInfoRepository userInfoRepository = new UserInfoRepository();
public OidcUserInfo loadUser(String username) {
return new OidcUserInfo(this.userInfoRepository.findByUsername(username));
}
static class UserInfoRepository {
private final Map<String, Map<String, Object>> userInfo = new HashMap<>();
public UserInfoRepository() {
this.userInfo.put("sry", createUser("sry"));
this.userInfo.put("sry", createUser("sry"));
}
public Map<String, Object> findByUsername(String username) {
return this.userInfo.get(username);
}
private static Map<String, Object> createUser(String username) {
return OidcUserInfo.builder()
.subject(username)
.name("First Last")
.givenName("First")
.familyName("Last")
.middleName("Middle")
.nickname("User")
.preferredUsername(username)
.profile("https://example.com/" + username)
.picture("https://example.com/" + username + ".jpg")
.website("https://example.com")
.email(username + "@example.com")
.emailVerified(true)
.gender("female")
.birthdate("1970-01-01")
.zoneinfo("Europe/Paris")
.locale("en-US")
.phoneNumber("+1 (604) 555-1234;ext=5678")
.claim("address", Collections.singletonMap("formatted", "Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"))
.updatedAt("1970-01-01T00:00:00Z")
.build()
.getClaims();
}
}
}
每个注册客户端都可以单独的配置超时时间,参考注释部分,
@Bean // 4
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("messaging-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:8090/login/oauth2/code/messaging-client")
.redirectUri("https://www.baidu.com")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("message.read")
.scope("message.write")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
// 设置token有效时间,设置刷新token的有效事假,设置刷新refresh_token是否重复使用,更多参考TokenSettings.builder()中的设置,里面还有token格式化的类型
.tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofMinutes(60))
.refreshTokenTimeToLive(Duration.ofMinutes(120))
.reuseRefreshTokens(false).build())
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
授权码请求的返回记录
{
"access_token": "eyJraWQiOiI3Y2FhMDI3MC0xNWM3LTQ0NTgtYWQxMi1iNmFhNTg5ZmE1MWMiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzcnkiLCJhdWQiOiJtZXNzYWdpbmctY2xpZW50IiwibmJmIjoxNjY5ODg5NzU2LCJzY29wZSI6WyJvcGVuaWQiLCJwcm9maWxlIiwibWVzc2FnZS5yZWFkIiwibWVzc2FnZS53cml0ZSJdLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJleHAiOjE2Njk4OTAwNTYsImNsYWltLTEiOiJ2YWx1ZS0xIiwiaWF0IjoxNjY5ODg5NzU2fQ.iObZNUDUYlkJl39ksOhV6PrM-wNwxwHAohMjUGFumwJqZr8d2NrASqRD839VtDvr717EzdXSEZpZNV6h6TMRizDE-fVjDdHdmGcd-ANwzCrDqGYLi5UE4nZ8drWLvhPW4QNCwFKDCOjJ6Mq_QPs_XN3lx2AXRuZW0GMAJx8NhIzGitgr9l0zky8BIBKyQRnFjej7GOo7zQb24-tvOwaGyfmCo2MKu6ZMe_l7Y5u-T6twOeaWFiP75KL41QyJZO3hCAgqKysszVJITezITCjJLrHHibv7adlE2N23mVZhb2pVVve8Y3ji--Qawa71v6P9f7Ax_SDzgo4OUzH95KnwWA",
"refresh_token": "_-g2uERYiodVRQJ5r5E1peVPDHmVTFEDoVW3pr0HVkwIx_zY_pIDl7M5UYUarAKCkqKo-xeJ2rIBiln7CwCGi-mbGsTbCGwJn6Pzj-M5yB4U0tTQ1bFe54EI3AV7ateF",
"scope": "openid profile message.read message.write",
"id_token": "eyJraWQiOiI3Y2FhMDI3MC0xNWM3LTQ0NTgtYWQxMi1iNmFhNTg5ZmE1MWMiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzcnkiLCJ6b25laW5mbyI6IkV1cm9wZS9QYXJpcyIsImJpcnRoZGF0ZSI6IjE5NzAtMDEtMDEiLCJnZW5kZXIiOiJmZW1hbGUiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzcnkiLCJsb2NhbGUiOiJlbi1VUyIsInVwZGF0ZWRfYXQiOiIxOTcwLTAxLTAxVDAwOjAwOjAwWiIsImF6cCI6Im1lc3NhZ2luZy1jbGllbnQiLCJuaWNrbmFtZSI6IlVzZXIiLCJleHAiOjE2Njk4OTE1NjIsImlhdCI6MTY2OTg4OTc2MiwiZW1haWwiOiJzcnlAZXhhbXBsZS5jb20iLCJ3ZWJzaXRlIjoiaHR0cHM6Ly9leGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhZGRyZXNzIjp7ImZvcm1hdHRlZCI6IkNoYW1wIGRlIE1hcnNcbjUgQXYuIEFuYXRvbGUgRnJhbmNlXG43NTAwNyBQYXJpc1xuRnJhbmNlIn0sInByb2ZpbGUiOiJodHRwczovL2V4YW1wbGUuY29tL3NyeSIsImdpdmVuX25hbWUiOiJGaXJzdCIsIm1pZGRsZV9uYW1lIjoiTWlkZGxlIiwicGljdHVyZSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vc3J5LmpwZyIsImF1ZCI6Im1lc3NhZ2luZy1jbGllbnQiLCJuYW1lIjoiRmlyc3QgTGFzdCIsInBob25lX251bWJlciI6IisxICg2MDQpIDU1NS0xMjM0O2V4dD01Njc4IiwiZmFtaWx5X25hbWUiOiJMYXN0In0.BL1TuwdfIqvAM5sfMLd3s3NvNipednsgZdvNEcO6_PyXCR9i7PIy4MvqzRdZfuoNVoJa16qL1e1wGuI1pNp21rk8PMSBusO1jNOSbYGoI5hFaBMeWFKQgYJc-i5e8kI5yyd-Nar6xuDYSgijoe1Wte4UxhFM1Jeuq7WjcCl_AaevNZM7WzV2914oTVismOCYOkLHw-ISHopifOi4TfW3vOAVZfZFKNdGNIwbNMEZw8YGM4tI_MDF6CBfbmqiWWGOihINEiBPmYn2RIqRSCSMsFfuXpdYAm-zlEpXMjgWl-X2ENwjYI5XbOgfOf-zfFQ0jPbVMhaduj5GXY3y9YkImA",
"token_type": "Bearer",
"expires_in": 3599
}
单点登录实现方式概述
oauth2实现单点登录
主要是查看之前 authorization server && client && resource 使用1 中的示例
就客户端而言,两个客户端使用相同的clientId和clientSecret,客户端的地址可以不一样(一定要能重定向回来),服务端的重定向地址必须包含多个客户端配置,人后以下是测试的客户端配置
@Value("${server.port}")
private String port;
// 尝试通过http://localhost:8080/.well-known/openid-configuration获取相关接口
private ClientRegistration localClientRegistration() {
return ClientRegistration.withRegistrationId("messaging-client")
.clientId("messaging-client")
.clientSecret("secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("http://127.0.0.1:"+port+"/login/oauth2/code/messaging-client")
.scope("openid", "profile", "message.read", "message.write")
.authorizationUri("http://127.0.0.1:8080/oauth2/authorize")
.tokenUri("http://127.0.0.1:8080/oauth2/token")
.userInfoUri("http://127.0.0.1:8080/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("http://127.0.0.1:8080/oauth2/jwks")
.clientName("messaging-client")
.build();
}
由于请求的session存储的cookie是基于ip的,所以本地测试修改session存储的cookie的名称需要修改,或者修改hosts
server:
port: 8090
servlet:
#防止Cookie冲突,冲突会导致登录验证不通过
session:
cookie:
name: OAUTH2-CLIENT-SESSIONID${server.port}
什么都不需要修改,也不需要@EnableOAuth2Sso注解。然后就可以实现单点登录,
成功测试
当然上述是基于授权服务端的用户登录是存放cookie的,如果使用JWT,可能需要另外考虑
CAS(中央认证服务)
发现这个需要搭建一个cas的服务端,而security只是支持我们去连接cas服务器,。。。。。无语,以为服务端和客户端security都实现的
security cas客户端认证流程
官方文档
web浏览器、CAS服务器和Spring Security安全服务之间的基本交互如下
1、用户访问需要认证的服务,抛出异常,ExceptionTranslationFilter进行处理
2、如果使用了cas,那么ExceptionTranslationFilter将会调用CasAuthenticationEntryPoint进行身份认证
3、CasAuthenticationEntryPoint将会重定向到 对应的cas认证页面,例如my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas.
4、如果存在session cookie ,那么用户不需要再次进行认证,会从定向会原有的服务地址,附带令牌,例如server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ.
5、CasAuthenticationFilter会 处理这个请求,请求验证后会被配置的AuthenticationManager 进行处理
6、然后CasAuthenticationProvider中会使用ticket请求cas服务器my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/login/cas/proxyreceptor.
7、如果对CAS验证服务的请求包含代理回调URL(在pgt-URL参数中),CAS将在XML响应中包含pgt-Iou字符串。此pgt Iou代表授权票据Iou的代理。然后,CAS服务器将创建自己的HTTPS连接返回pgt Url。这是为了相互验证CAS服务器和声明的服务URL。如果cas验证成功,HTTPS连接将用于向原始web应用程序发送代理授权票证。例如: server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH.
8、Cas20Ticket Validator将分析从CAS服务器接收到的XML。它将向CasAuthenticationProvider返回一个票据响应,其中包括用户名(必填)、代理列表(如果涉及)和代理授权票据欠条(如果请求了代理回调)。
9、CasAuthenticationProvider中的CasProxyDecider会验证cas响应的票据,CasProxyDecider有这几个实现RejectProxyTickets,
AcceptAnyCasProxyand
NamedCasProxyDecider
10、如果验证没有问题CasAuthenticationProvider会返回一个CasAuthenticationToken
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
cas服务端搭建
项目地址:https://github.com/apereo/cas-overlay-template
项目文档:https://apereo.github.io/cas/6.6.x/index.html
最新的版本似乎是需要jdk17,而且需要使用gradle。
其他
算了,仅仅了解这么多,以后再说。
SAML2单点登录
总是SAML2是实现单点登录的,记录一下
gateway和security
仅做说明,未尝试
1、并不是多有服务都需要用户登录后访问,所以部分服务是可以直接放通访问权限
2、对于和用户关联的服务,肯定需要认证后才可以访问,一个是通过结合授权中心,不过最新的spring-authorization-server需要用户登录的模式只有授权码模式,这样oauth2就是作为授权服务器的客户端,整合spring-boot-starter-oauth2-client,这样所有访问网关的服务都会重定向到授权服务进行认证,认证通过后才能访问其他服务,而其他微服务整合oauth2-resource。
3、就淘宝和京东而言,似乎登录虽然重定向了,但是明显不是授权码模式,而是直接登录,然后登录的令牌相关的cookie设置在根域名下,然后所有服务都可以获取到相关的token,从而获取用户信息。 我们可以单独提供认证的服务,网关中设置全局过滤器,如果没有认证,就重定向到认证服务,
4、由于gateway使用的是webflux,相关security配置项查看https://docs.spring.io/spring-security/reference/5.8/reactive/configuration/webflux.html和https://docs.spring.io/spring-security/reference/5.8/reactive/index.html
关联
- 关联的主题:
- 上一篇:
- 下一篇:
- image: 20221021/1
- 转载自: