文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client

2023-08-18 17:50

关注

title: OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client
date: 2023-03-27 01:41:26
tags:


1. 授权服务器

目前 Spring 生态中的 OAuth2 授权服务器是 Spring Authorization Server,原先的 Spring Security OAuth 已经停止更新

1.1 引入依赖

这里的 spring-security-oauth2-authorization-server 用的是 0.4.0 版本,适配 JDK 1.8,Spring Boot 版本为 2.7.7

                    org.springframework.boot            spring-boot-starter-security                            org.springframework.security            spring-security-oauth2-authorization-server                            org.springframework.boot            spring-boot-starter-web                            org.springframework.boot            spring-boot-starter-thymeleaf                            org.springframework.boot            spring-boot-starter-jdbc                            mysql            mysql-connector-java        

1.2 配置类

可以参考官方的 Samples:spring-authorization-server/samples

1.2.1 最小配置

官网最小配置 Demo 地址:Getting Started

官网最小配置如下,通过添加该配置类,启动项目,这就能够完成 OAuth2 的授权

@Configurationpublic class SecurityConfig {@Bean @Order(1)public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)throws Exception {OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());// Enable OpenID Connect 1.0http// Redirect to the login page when not authenticated from the// authorization endpoint.exceptionHandling((exceptions) -> exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")))// Accept access tokens for User Info and/or Client Registration.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);return http.build();}@Bean @Order(2)public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)throws Exception {http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())// Form login handles the redirect to the login page from the// authorization server filter chain.formLogin(Customizer.withDefaults());return http.build();}@Bean public UserDetailsService userDetailsService() {UserDetails userDetails = User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build();return new InMemoryUserDetailsManager(userDetails);}@Bean 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:8080/login/oauth2/code/messaging-client-oidc").redirectUri("http://127.0.0.1:8080/authorized").scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE).scope("message.read").scope("message.write").clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).build();return new InMemoryRegisteredClientRepository(registeredClient);}@Bean public JWKSource jwkSource() {KeyPair keyPair = generateRsaKey();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);}private static KeyPair generateRsaKey() { KeyPair keyPair;try {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");keyPairGenerator.initialize(2048);keyPair = keyPairGenerator.generateKeyPair();}catch (Exception ex) {throw new IllegalStateException(ex);}return keyPair;}@Bean public JwtDecoder jwtDecoder(JWKSource jwkSource) {return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);}@Bean public AuthorizationServerSettings authorizationServerSettings() {return AuthorizationServerSettings.builder().build();}}

在上面的 Demo 里,将所有配置都写在了一个配置类 SecurityConfig 里,实际上 Spring Authorization Server 还提供了一种实现最小配置的默认配置形式,就是通过 OAuth2AuthorizationServerConfiguration 这个类,源码如下:

@Configuration(proxyBeanMethods = false)public class OAuth2AuthorizationServerConfiguration {@Bean@Order(Ordered.HIGHEST_PRECEDENCE)public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {applyDefaultSecurity(http);return http.build();}// @formatter:offpublic static void applyDefaultSecurity(HttpSecurity http) throws Exception {OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =new OAuth2AuthorizationServerConfigurer();RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();http.requestMatcher(endpointsMatcher).authorizeRequests(authorizeRequests ->authorizeRequests.anyRequest().authenticated()).csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)).apply(authorizationServerConfigurer);}// @formatter:onpublic static JwtDecoder jwtDecoder(JWKSource jwkSource) {Set jwsAlgs = new HashSet<>();jwsAlgs.addAll(JWSAlgorithm.Family.RSA);jwsAlgs.addAll(JWSAlgorithm.Family.EC);jwsAlgs.addAll(JWSAlgorithm.Family.HMAC_SHA);ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>();JWSKeySelector jwsKeySelector =new JWSVerificationKeySelector<>(jwsAlgs, jwkSource);jwtProcessor.setJWSKeySelector(jwsKeySelector);// Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it insteadjwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {});return new NimbusJwtDecoder(jwtProcessor);}@BeanRegisterMissingBeanPostProcessor registerMissingBeanPostProcessor() {RegisterMissingBeanPostProcessor postProcessor = new RegisterMissingBeanPostProcessor();postProcessor.addBeanDefinition(AuthorizationServerSettings.class, () -> AuthorizationServerSettings.builder().build());return postProcessor;}}

这里注入一个叫做 authorizationServerSecurityFilterChain 的 bean,其实对比一下可以看出,这和最小配置的实现基本是相同的。有了这个 bean,就会支持如下协议端点:

接下来使用 OAuth2AuthorizationServerConfiguration 这个类来实现一个 Authorization Server,将 Spring Security 和 Authorization Server 的配置分开,Spring Security 使用 SecurityConfig 类,创建一个新的Authorization Server 配置类 AuthorizationServerConfig

1.2.2 ServerSecurityConfig

@EnableWebSecurity@Configuration(proxyBeanMethods = false)public class ServerSecurityConfig {    @Resource    private DataSource dataSource;        @Bean    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {        http.authorizeHttpRequests(authorize -> authorize                        // 配置放行的请求                        .antMatchers("/api    @Bean    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {        return new JdbcRegisteredClientRepository(jdbcTemplate);    }        @Bean    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);    }        @Bean    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);    }        @Bean    public JWKSource jwkSource() {        KeyPair keyPair;        try {            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");            keyPairGenerator.initialize(2048);            keyPair = keyPairGenerator.generateKeyPair();        } catch (Exception ex) {            throw new IllegalStateException(ex);        }        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 (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);    }        @Bean    public JwtDecoder jwtDecoder(JWKSource jwkSource) {        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);    }        @Bean    public AuthorizationServerSettings authorizationServerSettings() {        return AuthorizationServerSettings.builder().build();    }}

1.3 创建数据库表

一共包括 5 个表,其中 Spring Security 相关的有 2 个表,user 和 authorities,用户表和权限表,该表的建表 SQL 在

org\springframework\security\core\userdetails\jdbc\users.ddl

SQL 可能会有一些问题,根据自己使用的数据库进行更改

create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null);create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username));create unique index ix_auth_username on authorities (username,authority);

Spring authorization Server 有 3 个表,建表 SQL 在:

org\springframework\security\oauth2\server\authorization\oauth2-authorization-consent-schema.sql

org\springframework\security\oauth2\server\authorization\oauth2-authorization-schema.sql

org\springframework\security\oauth2\server\authorization\client\oauth2-registered-client-schema.sql

CREATE TABLE oauth2_authorization_consent (    registered_client_id varchar(100) NOT NULL,    principal_name varchar(200) NOT NULL,    authorities varchar(1000) NOT NULL,    PRIMARY KEY (registered_client_id, principal_name));
CREATE TABLE oauth2_authorization (    id varchar(100) NOT NULL,    registered_client_id varchar(100) NOT NULL,    principal_name varchar(200) NOT NULL,    authorization_grant_type varchar(100) NOT NULL,    authorized_scopes varchar(1000) DEFAULT NULL,    attributes blob DEFAULT NULL,    state varchar(500) DEFAULT NULL,    authorization_code_value blob DEFAULT NULL,    authorization_code_issued_at timestamp DEFAULT NULL,    authorization_code_expires_at timestamp DEFAULT NULL,    authorization_code_metadata blob DEFAULT NULL,    access_token_value blob DEFAULT NULL,    access_token_issued_at timestamp DEFAULT NULL,    access_token_expires_at timestamp DEFAULT NULL,    access_token_metadata blob DEFAULT NULL,    access_token_type varchar(100) DEFAULT NULL,    access_token_scopes varchar(1000) DEFAULT NULL,    oidc_id_token_value blob DEFAULT NULL,    oidc_id_token_issued_at timestamp DEFAULT NULL,    oidc_id_token_expires_at timestamp DEFAULT NULL,    oidc_id_token_metadata blob DEFAULT NULL,    refresh_token_value blob DEFAULT NULL,    refresh_token_issued_at timestamp DEFAULT NULL,    refresh_token_expires_at timestamp DEFAULT NULL,    refresh_token_metadata blob DEFAULT NULL,    PRIMARY KEY (id));
CREATE TABLE oauth2_registered_client (    id varchar(100) NOT NULL,    client_id varchar(100) NOT NULL,    client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,    client_secret varchar(200) DEFAULT NULL,    client_secret_expires_at timestamp DEFAULT NULL,    client_name varchar(200) NOT NULL,    client_authentication_methods varchar(1000) NOT NULL,    authorization_grant_types varchar(1000) NOT NULL,    redirect_uris varchar(1000) DEFAULT NULL,    scopes varchar(1000) NOT NULL,    client_settings varchar(2000) NOT NULL,    token_settings varchar(2000) NOT NULL,    PRIMARY KEY (id));

创建完成后的数据库表如下:

1.4 自定义登录和授权页面

在项目 resource 目录下创建一个 templates 文件夹,然后创建 login.html 和 consent.html,登录页面的配置在 1.2.2 中配置好了,授权页面的配置在 1.2.3 中配置好了

登录页面 login.html

            Spring Security Example        

创建 LoginConroller,用于跳转到 login.html 页面

@Controllerpublic class LoginController {@GetMapping("/login")public String login() {return "login";}}

授权页面 consent.html

                Custom consent page - Consent required    

应用程序权限

应用程序 想要访问您的帐户

上述应用程序请求以下权限
如果您批准,请查看这些并同意

您已向上述应用授予以下权限:

Your consent to provide access is required.
If you do not approve, click Cancel, in which case no information will be shared with the app.

创建 AuthorizationConsentController,用于跳转到 consent.html 页面

@Controllerpublic class AuthorizationConsentController {private final RegisteredClientRepository registeredClientRepository;private final OAuth2AuthorizationConsentService authorizationConsentService;public AuthorizationConsentController(RegisteredClientRepository registeredClientRepository,OAuth2AuthorizationConsentService authorizationConsentService) {this.registeredClientRepository = registeredClientRepository;this.authorizationConsentService = authorizationConsentService;}@GetMapping(value = "/oauth2/consent")public String consent(Principal principal, Model model,@RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,@RequestParam(OAuth2ParameterNames.SCOPE) String scope,@RequestParam(OAuth2ParameterNames.STATE) String state) {// 要批准的范围和以前批准的范围Set scopesToApprove = new HashSet<>();Set previouslyApprovedScopes = new HashSet<>();// 查询 clientId 是否存在RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);// 查询当前的授权许可OAuth2AuthorizationConsent currentAuthorizationConsent =this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());// 已授权范围Set authorizedScopes;if (currentAuthorizationConsent != null) {authorizedScopes = currentAuthorizationConsent.getScopes();} else {authorizedScopes = Collections.emptySet();}for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {if (OidcScopes.OPENID.equals(requestedScope)) {continue;}// 如果已授权范围包含了请求范围,则添加到以前批准的范围的 Set, 否则添加到要批准的范围if (authorizedScopes.contains(requestedScope)) {previouslyApprovedScopes.add(requestedScope);} else {scopesToApprove.add(requestedScope);}}model.addAttribute("clientId", clientId);model.addAttribute("state", state);model.addAttribute("scopes", withDescription(scopesToApprove));model.addAttribute("previouslyApprovedScopes", withDescription(previouslyApprovedScopes));model.addAttribute("principalName", principal.getName());return "consent";}private static Set withDescription(Set scopes) {Set scopeWithDescriptions = new HashSet<>();for (String scope : scopes) {scopeWithDescriptions.add(new ScopeWithDescription(scope));}return scopeWithDescriptions;}public static class ScopeWithDescription {private static final String DEFAULT_DESCRIPTION = "未知范围 - 我们无法提供有关此权限的信息, 请在授予此权限时谨慎";private static final Map scopeDescriptions = new HashMap<>();static {scopeDescriptions.put(OidcScopes.PROFILE,"此应用程序将能够读取您的个人资料信息");scopeDescriptions.put("message.read","此应用程序将能够读取您的信息");scopeDescriptions.put("message.write","此应用程序将能够添加新信息, 它还可以编辑和删除现有信息");scopeDescriptions.put("other.scope","这是范围描述的另一个范围示例");}public final String scope;public final String description;ScopeWithDescription(String scope) {this.scope = scope;this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION);}}}

1.5 ServerController

用于添加用户信息和客户端信息,这里的 passwordEncoder 使用 BCryptPasswordEncoder 进行加解密,{bcrypt} 表示加密,{noop} 表示明文

@RestControllerpublic class ServerController {    @Resource    private UserDetailsManager userDetailsManager;    @GetMapping("/api/addUser")    public String addUser() {        UserDetails userDetails = User.builder().passwordEncoder(s -> "{bcrypt}" + new BCryptPasswordEncoder().encode(s))                .username("fan")                .password("fan")                .roles("ADMIN")                .build();        userDetailsManager.createUser(userDetails);        return "添加用户成功";    }    @Resource    private RegisteredClientRepository registeredClientRepository;    @GetMapping("/api/addClient")    public String addClient() {        // JWT(Json Web Token)的配置项:TTL、是否复用refreshToken等等        TokenSettings tokenSettings = TokenSettings.builder()                // 令牌存活时间:2小时                .accessTokenTimeToLive(Duration.ofHours(2))                // 令牌可以刷新,重新获取                .reuseRefreshTokens(true)                // 刷新时间:30天(30天内当令牌过期时,可以用刷新令牌重新申请新令牌,不需要再认证)                .refreshTokenTimeToLive(Duration.ofDays(30))                .build();        // 客户端相关配置        ClientSettings clientSettings = ClientSettings.builder()                // 是否需要用户授权确认                .requireAuthorizationConsent(true)                .build();        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())                // 客户端ID和密码                .clientId("messaging-client")//                .clientSecret("{bcrypt}" + new BCryptPasswordEncoder().encode("secret"))                .clientSecret("{noop}secret")                // 授权方法                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)                // 授权模式(授权码模式)                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)                // 刷新令牌(授权码模式)                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)                // 回调地址:授权服务器向当前客户端响应时调用下面地址, 不在此列的地址将被拒绝, 只能使用IP或域名,不能使用 localhost                .redirectUri("http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc")                // OIDC 支持                .scope(OidcScopes.OPENID)                .scope(OidcScopes.PROFILE)                // 授权范围(当前客户端的授权范围)                .scope("message.read")                .scope("message.write")                // JWT(Json Web Token)配置项                .tokenSettings(tokenSettings)                // 客户端配置项                .clientSettings(clientSettings)                .build();        registeredClientRepository.save(registeredClient);        return "添加客户端信息成功";    }}

1.6 YAML 配置

配置数据库连接信息

server:  port: 9000spring:  datasource:    url: jdbc:mysql://localhost:3306/unified_certification?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC    driver-class-name: com.mysql.cj.jdbc.Driver    username: root    password: root

1.7 测试

完整目录结构如下:

1.7.1 添加用户和客户端信息

启动项目,访问 http://127.0.0.1:9000/api/addUser

查询数据库 users 和 authorities 表,已有用户和权限信息

访问 http://127.0.0.1:9000/api/addClient

查询数据库 oauth2_registered_client 表,已有客户端信息

1.7.2 授权码模式获取令牌

有关 OAuth2.0 的相关知识可见:OAuth2.0 实战总结_凡 223 的博客

访问 http://127.0.0.1:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=message.read&redirect_uri=http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc,这里的 127.0.0.1:8000 其实为客户端地址,后面讲到客户端时,客户端的地址就为 8000

未登录,会跳转到登录页面

输入前面添加的用户信息,用户名和密码,然后会跳转到授权页面

选择是否授予权限,这里勾选后,点击提交,会跳转到回调地址,即 127.0.0.1:8000/login/oauth2/code/messaging-client-oidc,由于这个地址还没有对应的服务,无法访问,但我们暂时需要的是地址栏的 code

http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc?code=z_3O1lEdxVsd2fn8_uKA481pO9caGd0N4x_Vbt0deuMA77sDis6fhMJkf2_9uM4KGYzLzv7ujbXZ2JAdg0ACyMapR38jnJruG2iz2XBgptKrru-IJobGVa6NTicgvCZ7

打开接口测试工具,这里我使用的是 Apifox,使用表单格式,包含三个参数

然后设置 Auth,Postman 里是 Authorization,选择 Basic Auth 类型,用户名密码则为注册客户端时的 client_id 和 clientSecret,客户端 ID 和密钥

保存,发送后,会给我们返回 access_token 和 refresh_token

将 access_token 复制到 JSON Web Tokens - jwt.io 网站,解析后可以看到 JWT 的信息,包括客户端 ID,权限范围,服务器地址等

1.7.3 授权码模式刷新令牌

在前面返回了 access_token 和 refresh_token,access_token 包含了授权信息,refresh_token 则是用来重新获取 access_token,同样是表单类型,包含两个参数

Auth 信息与前面一致

保存,发送后,会给我们返回新的 access_token 和 refresh_token,refresh_token 使用一次就会失效

1.7.4 客户端模式

同样使用表单格式,grant_type 值为 client_credentials

Auth 与前面一致

保存,发送后,会给我们返回 access_token,没有 refresh_token。因为在授权码模式中的 access_token 是我们通过授权码 code 换来的,而授权码 code 是我们请求后授权得到的,为了不用每次获取 access_token 都需要重新请求授权,所以使用 refresh_token 来重新获取 access_token,refresh_token 和 access_token 都有过期时间,refresh_token 过期时间比 access_token 长

而客户端模式可以直接获取 access_token,所以也就不需要 refresh_token 了

1.7.5 OIDC

有关 OIDC 的相关知识同样可见:OAuth2.0 实战总结_凡 223 的博客

在前面 1.2.3 的配置和 1.5 的注册客户端时,已经支持了 OIDC,这里直接访问:http://127.0.0.1:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid&redirect_uri=http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc

这里的 scope 必须包含 openid

得到授权码 code

http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc?code=NjvT1z3msYRsjvPPM4LP4EmlyBUixsKes_J6osSB3VAugXEKmyUappvtrmTWp7s_iQzoJsD8xOE3gUXawhMixL0fu2HC6UJv8CeZyCB-d2oiu4NnCO9uJcK1MXOm4poU

然后通过授权码 code 换取令牌,可以看到除了 access_token 和 refresh_token 外,还返回了一个 id_token

解析这个 id_token,信息如下,是我们的身份认证信息

再通过 refresh_token 重新获取令牌,同样也给我们返回了 id_token

通过 access_token,获取 OIDC 的用户端点

这里的 sub 就是用户的标志。在 1.2.3 的配置中,对于 OIDC 使用的是默认配置

我们也可以增加自定义信息,修改后的配置如下,其他配置不变

@Configuration(proxyBeanMethods = false)public class AuthorizationServerConfig {    private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";    @Bean    @Order(Ordered.HIGHEST_PRECEDENCE)    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {        // 定义授权服务配置器        OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer();        configurer                // 自定义授权页面                .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI))                // Enable OpenID Connect 1.0, 启用 OIDC 1.0                .oidc(oidcConfigurer -> oidcConfigurer.userInfoEndpoint(userInfoEndpointConfigurer ->                        userInfoEndpointConfigurer.userInfoMapper(userInfoAuthenticationContext -> {OAuth2AccessToken accessToken = userInfoAuthenticationContext.getAccessToken();Map claims = MapUtil.map(false);claims.put("url", "http://127.0.0.1:9000");claims.put("accessToken", accessToken);claims.put("sub", userInfoAuthenticationContext.getAuthorization().getPrincipalName());return new OidcUserInfo(claims);                        })));        // 获取授权服务器相关的请求端点        RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher();        http                // 拦截对授权服务器相关端点的请求                .requestMatcher(endpointsMatcher)                // 拦载到的请求需要认证                .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())                // 忽略掉相关端点的 CSRF(跨站请求): 对授权端点的访问可以是跨站的                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)                // 访问端点时表单登录                .formLogin()                .and()                // 应用授权服务器的配置                .apply(configurer);        return http.build();    }    // ... 其他配置不变}

重启项目,重新获取到 access_token,通过 access_token 访问用户端点,可以看到我们自定义的信息已经被添加了进来

2. 资源服务器

2.1 引入依赖

                    org.springframework.boot            spring-boot-starter-web                            org.springframework.boot            spring-boot-starter-security                            org.springframework.boot            spring-boot-starter-oauth2-resource-server                            cn.hutool            hutool-all        

2.2 YAML 配置

server:  port: 8001spring:  security:    oauth2:      resourceserver:        jwt:          issuer-uri: http://localhost:9000

2.3 异常处理器

该部分为 Spring Security 相关知识,可见:Spring Security 总结_凡 223 的博客

2.3.1 认证失败处理器

Response 为自定义的统一结果返回类,这里的返回信息自定义即可

public class UnAccessDeniedHandler implements AccessDeniedHandler {    @Override    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {        // 403, 未授权, 禁止访问        response.setStatus(HttpServletResponse.SC_FORBIDDEN);        response.setCharacterEncoding("utf-8");        response.setContentType(MediaType.APPLICATION_JSON_VALUE);        // 返回响应信息        ServletOutputStream outputStream = response.getOutputStream();        Response fail = Response.fail(HttpServletResponse.SC_FORBIDDEN,                "UnAccessDeniedHandler-未授权, 不允许访问", "uri-" + request.getRequestURI());        outputStream.write(JSONUtil.toJsonStr(fail).getBytes(StandardCharsets.UTF_8));        // 关闭流        outputStream.flush();        outputStream.close();    }}

2.3.2 鉴权失败处理器

public class UnAuthenticationEntryPoint implements AuthenticationEntryPoint {    @Override    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {        if (authException instanceof InvalidBearerTokenException) {            LogUtil.info("Token 登录失效");        }        if (response.isCommitted()) {            return;        }        // 401, 未认证        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);        response.setStatus(HttpServletResponse.SC_ACCEPTED);        response.setCharacterEncoding("utf-8");        response.setContentType(MediaType.APPLICATION_JSON_VALUE);        // 返回响应信息        ServletOutputStream outputStream = response.getOutputStream();        Response fail = Response.fail(HttpServletResponse.SC_UNAUTHORIZED,                authException.getMessage() + "-UnAuthenticationEntryPoint-认证失败", "uri-" + request.getRequestURI());        outputStream.write(JSONUtil.toJsonStr(fail).getBytes(StandardCharsets.UTF_8));        // 关闭流        outputStream.flush();        outputStream.close();    }}

2.4 配置类

对资源请求配置了读、写、profile 权限

@EnableWebSecurity@Configuration(proxyBeanMethods = false)public class ResourceServerConfig {        @Bean    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {        UnAuthenticationEntryPoint authenticationEntryPoint = new UnAuthenticationEntryPoint();        UnAccessDeniedHandler accessDeniedHandler = new UnAccessDeniedHandler();        http                // security的session生成策略改为security不主动创建session, 即STALELESS                // 资源服务不涉及用户登录, 仅靠token访问, 不需要seesion                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)                .and()                .authorizeHttpRequests(authorize -> authorize                        // 对 /resource1 的请求,需要 SCOPE_message.read 权限                        .antMatchers("/resource1").hasAuthority("SCOPE_message.read")                        // 对 /resource2 的请求,需要 SCOPE_message.write 权限                        .antMatchers("/resource2").hasAuthority("SCOPE_message.write")                        // 对 /resource3 的请求,需要 SCOPE_profile 权限                        .antMatchers("/resource3").hasAuthority("SCOPE_profile")                        // 放行请求                        .antMatchers("/api    @Bean    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {        http.authorizeHttpRequests(authorize ->                        // 任何请求都需要认证                        authorize.anyRequest().authenticated()                )                // 登录//                .oauth2Login(oauth2Login -> oauth2Login.loginPage("/oauth2/authorization/messaging-client-oidc"))                .oauth2Login(Customizer.withDefaults())                .oauth2Client(Customizer.withDefaults());        return http.build();    }}

3.4 index.html

        Title登录用户:

创建 IndexController,跳转到 index.html

@Controllerpublic class IndexController {@GetMapping("/")public String root() {return "redirect:/index";}@GetMapping("/index")public String index(Model model) {Map map = MapUtil.map(false);Authentication auth = SecurityContextHolder.getContext().getAuthentication();map.put("name", auth.getName());Collection authorities = auth.getAuthorities();List authoritiesList = authorities.stream().collect(Collectors.toList());map.put("authorities", authoritiesList);model.addAttribute("user", JSONUtil.toJsonStr(map));return "index";}}

3.5 ResourceController

@RestControllerpublic class ResourceController {    @GetMapping("/server/a/resource1")    public String getServerARes1(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {        return getServer("http://127.0.0.1:8001/resource1", oAuth2AuthorizedClient);    }    @GetMapping("/server/a/resource2")    public String getServerARes2(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {        return getServer("http://127.0.0.1:8001/resource2", oAuth2AuthorizedClient);    }    @GetMapping("/server/a/resource3")    public String getServerBRes1(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {        return getServer("http://127.0.0.1:8001/resource3", oAuth2AuthorizedClient);    }    @GetMapping("/server/a/publicResource")    public String getServerBRes2(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {        return getServer("http://127.0.0.1:8001/api/publicResource", oAuth2AuthorizedClient);    }        private String getServer(String url, OAuth2AuthorizedClient oAuth2AuthorizedClient) {        LogUtil.info("getServer");        // 获取 access_token        String tokenValue = oAuth2AuthorizedClient.getAccessToken().getTokenValue();        // 发起请求        Mono stringMono = WebClient.builder()                .defaultHeader("Authorization", "Bearer " + tokenValue)                .build()                .get()                .uri(url)                .retrieve()                .bodyToMono(String.class);        return stringMono.block();    }}

3.6 测试

完整目录结构如下:

启动项目,访问 127.0.0.1:8000,未登录会直接跳转到登录页面

输入用户名密码,登录后进入授权页面

选择想要授予的权限,这里勾选 read 权限,点击提交,跳转到我们的首页 index.html

将上面 user 的 JSON 信息格式化一下如下,可以看到就是我们的认证和权限信息

点击访问 服务A -> 资源1

点击访问 服务A -> 资源2,无法访问

这是因为之前授权时只给了 read 权限,而资源 2 需要 write 权限,可以看到报了 403 异常,这里可以定义一个异常处理类,来返回对应的信息,而不是白页

我们关闭当前页面新开一个页面,再次访问 127.0.0.1:8000 可以发现直接进入了 index.html,无需再次登录

可以发现我们访问时是带了一个 JESSEIONID 的,用户登录后,会在认证服务器和客户端都保存 session 信息

来源地址:https://blog.csdn.net/ACE_U_005A/article/details/128851814

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-服务器
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯