前言
为什么使用spring-authorization-server?
真实原因:原先是因为个人原因,需要研究新版鉴权服务,看到了spring-authorization-server
,使用过程中,想着能不能整合新版本cloud,因此此处先以springboot搭建spring-authorization-server
,后续再替换为springcloud2021。
官方原因:原先使用Spring Security OAuth
,而该项目已经逐渐被淘汰,虽然网上还是有不少该方案,但秉着技术要随时代更新,从而使用spring-authorization-server
Spring 团队正式宣布 Spring Security OAuth
停止维护,该项目将不会再进行任何的迭代
项目构建
以springboot搭建spring-authorization-server(即认证与资源服务器)
数据库相关表结构构建
需要创建3张表,sql分别如下
CREATE TABLE `oauth2_authorization` ( `id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `authorization_grant_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `attributes` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `state` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `authorization_code_value` blob NULL, `authorization_code_issued_at` timestamp(0) NULL DEFAULT NULL, `authorization_code_expires_at` timestamp(0) NULL DEFAULT NULL, `authorization_code_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `access_token_value` blob NULL, `access_token_issued_at` timestamp(0) NULL DEFAULT NULL, `access_token_expires_at` timestamp(0) NULL DEFAULT NULL, `access_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `access_token_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `access_token_scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `oidc_id_token_value` blob NULL, `oidc_id_token_issued_at` timestamp(0) NULL DEFAULT NULL, `oidc_id_token_expires_at` timestamp(0) NULL DEFAULT NULL, `oidc_id_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `refresh_token_value` blob NULL, `refresh_token_issued_at` timestamp(0) NULL DEFAULT NULL, `refresh_token_expires_at` timestamp(0) NULL DEFAULT NULL, `refresh_token_metadata` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; CREATE TABLE `oauth2_authorization_consent` ( `registered_client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `principal_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `authorities` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`registered_client_id`, `principal_name`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; CREATE TABLE `oauth2_registered_client` ( `id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `client_id_issued_at` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), `client_secret` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `client_secret_expires_at` timestamp(0) NULL DEFAULT NULL, `client_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `client_authentication_methods` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `authorization_grant_types` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `redirect_uris` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `scopes` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `client_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `token_settings` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
先进行认证服务器相关配置
pom.xml引入依赖
注意!!!spring boot版本需2.6.x以上,是为后面升级成cloud做准备
<dependency> <groupId>org.projectlombokgroupId> <artifactId>lombokartifactId> <version>1.18.22version> dependency> <dependency> <groupId>com.xxxx.iovgroupId> <artifactId>iov-cloud-framework-webartifactId> <version>2.0.0-SNAPSHOTversion> <exclusions> <exclusion> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-webartifactId> exclusion> exclusions> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-webartifactId> <version>2.6.6version> dependency> <dependency> <groupId>cn.hutoolgroupId> <artifactId>hutool-allartifactId> <version>5.8.0version> dependency> <dependency> <groupId>com.alibabagroupId> <artifactId>fastjsonartifactId> <version>1.2.39version> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-securityartifactId> dependency> <dependency> <groupId>org.springframework.securitygroupId> <artifactId>spring-security-oauth2-authorization-serverartifactId> <version>0.2.3version> dependency> <dependency> <groupId>org.springframework.securitygroupId> <artifactId>spring-security-casartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-thymeleafartifactId> dependency> <dependency> <groupId>com.alibabagroupId> <artifactId>druid-spring-boot-starterartifactId> <version>1.2.9version> dependency> <dependency> <groupId>mysqlgroupId> <artifactId>mysql-connector-javaartifactId> <version>8.0.28version> dependency> <dependency> <groupId>com.baomidougroupId> <artifactId>mybatis-plus-boot-starterartifactId> <version>3.5.1version> dependency> <dependency> <groupId>com.google.guavagroupId> <artifactId>guavaartifactId> <version>31.1-jreversion> dependency>
创建自定义登录页面 login.html (可不要,使用自带的登录界面)
html> <html lang="en" xmlns:th="https://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"> <head> <meta charset="utf-8"> <meta name="author" content="test"> <meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="description" content="This is a login page template based on Bootstrap 5"> <title>Login Pagetitle> <style> .is-invalid { color: red; } .invalid-feedback { color: red; } .mb-3 { margin-bottom: 3px; } style> <script th:inline="javascript"> /* // const baseURL = /*[[@{/}]]*/ ''; /*]]>*/ if (window !== top) { top.location.href = location.href; } script> head> <body class="hold-transition login-page"> <div class="login-box"> <div class="card"> <div class="card-body login-card-body"> <p class="login-box-msg">Sign in to start your sessionp> <div th:if="${param.error}" class="alert alert-error"> Invalid username and password. div> <div th:if="${param.logout}" class="alert alert-success"> You have been logged out. div> <form th:action="@{/login}" method="post" id="loginForm"> <div class="input-group mb-3"> <input type="text" class="form-control" value="zxg" name="username" placeholder="Email" autocomplete="off"> div> <div class="input-group mb-3"> <input type="password" id="password" name="password" value="123" class="form-control" maxlength="25" placeholder="Password" autocomplete="off"> div> <div class="row"> <div class="col-4"> <button type="submit" id="submitBtn">Sign Inbutton> div> div> form> <p class="mb-1"> <a href="javascript:void(0)">I forgot my passworda> p> <p class="mb-0"> <a href="javascript:void(0)" class="text-center">Register a new membershipa> p> div> div> div> <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js">script> <script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/3.1.0/jsencrypt.min.js">script> <script src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.9.0/jquery.validate.min.js">script> <script src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.9.0/additional-methods.min.js">script> <script th:inline="javascript"> $(function () { var encrypt = new JSEncrypt(); $.validator.setDefaults({ submitHandler: function (form) { console.log("Form successful submitted!"); form.submit(); } }); }); script> body> html>
创建自定义授权页面 consent.html(可不要,可使用自带的授权页面)
html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous"> <title>授权页面title> <style> body { background-color: aliceblue; } style> <script> function cancelConsent() { document.consent_form.reset(); document.consent_form.submit(); } script> head> <body> <div class="container"> <div class="py-5"> <h1 class="text-center text-primary">用户授权确认h1> div> <div class="row"> <div class="col text-center"> <p> 应用 <a href="https://felord.cn"><span class="font-weight-bold text-primary" th:text="${clientName}">span>a> 想要访问您的账号 <span class="font-weight-bold" th:text="${principalName}">span> p> div> div> <div class="row pb-3"> <div class="col text-center"><p>上述应用程序请求以下权限<br/>请审阅以下选项并勾选您同意的权限p>div> div> <div class="row"> <div class="col text-center"> <form name="consent_form" method="post" action="/oauth2/authorize"> <input type="hidden" name="client_id" th:value="${clientId}"> <input type="hidden" name="state" th:value="${state}"> <div th:each="scope: ${scopes}" class="form-group form-check py-1"> <input class="form-check-input" type="checkbox" name="scope" th:value="${scope.scope}" th:id="${scope.scope}"> <label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}">label> <p class="text-primary" th:text="${scope.description}">p> div> <p th:if="${not #lists.isEmpty(previouslyApprovedScopes)}">您已对上述应用授予以下权限:p> <div th:each="scope: ${previouslyApprovedScopes}" class="form-group form-check py-1"> <input class="form-check-input" type="checkbox" th:id="${scope.scope}" disabled checked> <label class="form-check-label font-weight-bold" th:for="${scope.scope}" th:text="${scope.scope}">label> <p class="text-primary" th:text="${scope.description}">p> div> <div class="form-group pt-3"> <button class="btn btn-primary btn-lg" type="submit" id="submit-consent"> 同意授权 button> div> <div class="form-group"> <button class="btn btn-link regular" type="button" id="cancel-consent" onclick="cancelConsent();"> 取消授权 button> div> form> div> div> <div class="row pt-4"> <div class="col text-center"> <p> <small> 需要您同意并提供访问权限。 <br/>如果您不同意,请单击<span class="font-weight-bold text-primary">取消授权span>,将不会为上述应用程序提供任何您的信息。 small> p> div> div> div> body> html>
修改配置文件 application.yml(配置内容可自行简略)
server: port: 9000 spring: application: name: authorization-server thymeleaf: cache: false datasource: url: jdbc:mysql://192.168.1.69:3306/test username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver security: oauth2: resourceserver: jwt: issuer-uri: http://127.0.0.1:9000 #认证中心端点,作为资源端的配置 application: security: excludeUrls: #excludeUrls中存放白名单地址 - "/favicon.ico" # mybatis plus配置 mybatis-plus: mapper-locations: classpath:/mapper/*Mapper.xml global-config: # 关闭MP3.0自带的banner banner: false db-config: #主键类型 0:"数据库ID自增", 1:"不操作", 2:"用户输入ID",3:"数字型snowflake", 4:"全局唯一ID UUID", 5:"字符串型snowflake"; id-type: AUTO #字段策略 insert-strategy: not_null update-strategy: not_null select-strategy: not_null #驼峰下划线w转换 table-underline: true # 逻辑删除配置 # 逻辑删除全局值(1表示已删除,这也是Mybatis Plus的默认配置) logic-delete-value: 1 # 逻辑未删除全局值(0表示未删除,这也是Mybatis Plus的默认配置) logic-not-delete-value: 0 configuration: #驼峰 map-underscore-to-camel-case: true #打开二级缓存 cache-enabled: true # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志
新增认证服务器配置文件 AuthorizationServerConfig
@Configuration(proxyBeanMethods = false) public class AuthorizationServerConfig { /** * 自定义授权页面 * 使用系统自带的即不用 */ private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent"; /** * 自定义UserDetailsService */ @Autowired private UserService userService; /** * * 使用默认配置进行form表单登录 * OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http) */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>(); authorizationServerConfigurer.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)); RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher(); http .requestMatcher(endpointsMatcher) .userDetailsService(userService) .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated()) .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) .apply(authorizationServerConfigurer); return http.formLogin(Customizer.withDefaults()).build(); } /** * 注册客户端应用 */ @Bean public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { // Save registered client in db as if in-jdbc RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("zxg") .clientSecret("123") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) // 回调地址 .redirectUri("http://www.baidu.com") // scope自定义的客户端范围 .scope(OidcScopes.OPENID) .scope("message.read") .scope("message.write") // client请求访问时需要授权同意 .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) // token配置项信息 .tokenSettings(TokenSettings.builder() // token有效期100分钟 .accessTokenTimeToLive(Duration.ofMinutes(100L)) // 使用默认JWT相关格式 .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED) // 开启刷新token .reuseRefreshTokens(true) // refreshToken有效期120分钟 .refreshTokenTimeToLive(Duration.ofMinutes(120L)) .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256).build() ) .build(); // Save registered client in db as if in-memory JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate); registeredClientRepository.save(registeredClient); return registeredClientRepository; } /** * 授权服务:管理OAuth2授权信息服务 */ @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); } /** * 加载JWK资源 * JWT:指的是 JSON Web Token,不存在签名的JWT是不安全的,存在签名的JWT是不可窜改的 * JWS:指的是签过名的JWT,即拥有签名的JWT * JWK:既然涉及到签名,就涉及到签名算法,对称加密还是非对称加密,那么就需要加密的 密钥或者公私钥对。此处我们将 JWT的密钥或者公私钥对统一称为 JSON WEB KEY,即 JWK。 */ @Bean public JWKSource jwkSource() { RSAKey rsaKey = JwksUtils.generateRsa(); JWKSet jwkSet = new JWKSet(rsaKey); return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); } /** * 配置 OAuth2.0 提供者元信息 */ @Bean public ProviderSettings providerSettings() { return ProviderSettings.builder().issuer("http://127.0.0.1:9000").build(); } }
新增Security的配置文件WebSecurityConfig
@Configuration @EnableWebSecurity(debug = true) //开启Security public class WebSecurityConfig { @Autowired private ApplicationProperties properties; /** * 设置加密方式 */ @Bean public PasswordEncoder passwordEncoder() { // // 将密码加密方式采用委托方式,默认以BCryptPasswordEncoder方式进行加密,兼容ldap,MD4,MD5等方式 // return PasswordEncoderFactories.createDelegatingPasswordEncoder(); // 此处我们使用明文方式 不建议这样 return NoOpPasswordEncoder.getInstance(); } /** * 使用WebSecurity.ignoring()忽略某些URL请求,这些请求将被Spring Security忽略 */ @Bean WebSecurityCustomizer webSecurityCustomizer() { return new WebSecurityCustomizer() { @Override public void customize(WebSecurity web) { // 读取配置文件application.security.excludeUrls下的链接进行忽略 web.ignoring().antMatchers(properties.getSecurity().getExcludeUrls().toArray(new String[]{})); } }; } /** * 针对http请求,进行拦截过滤 * * CookieCsrfTokenRepository进行CSRF保护的工作方式: * 1.客户端向服务器发出GET请求,例如请求主页 * 2.Spring发送 GET 请求的响应以及 Set-cookie 标头,其中包含安全生成的XSRF令牌 */ @Bean public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity .authorizeRequests(authorizeRequests -> authorizeRequests.antMatchers("/login").permitAll() .anyRequest().authenticated() ) //使用默认登录页面 //.formLogin(withDefaults()) //设置form登录,设置且放开登录页login .formLogin(fromlogin -> fromlogin.loginPage("/login").permitAll()) // Spring Security CSRF保护 .csrf(csrfToken -> csrfToken.csrfTokenRepository(new CookieCsrfTokenRepository())) // //开启认证服务器的资源服务器相关功能,即需校验token // .oauth2ResourceServer() // .accessDeniedHandler(new SimpleAccessDeniedHandler()) // .authenticationEntryPoint(new SimpleAuthenticationEntryPoint()) // .jwt() ; return httpSecurity.build(); } }
新增读取application配置的类 ApplicationProperties
/** * 此步主要是获取配置文件中配置的白名单,可自行舍去或自定义实现其他方式 **/ @Data @Component @ConfigurationProperties("application") public class ApplicationProperties { private final Security security = new Security(); @Data public static class Security { private Oauth2 oauth2; private List excludeUrls = new ArrayList<>(); @Data public static class Oauth2 { private String issuerUrl; } } }
新增 JwksUtils 类和 KeyGeneratorUtils
,这两个类作为JWT对称加密
public final class JwksUtils { private JwksUtils() { } /** * 生成RSA加密key (即JWK) */ public static RSAKey generateRsa() { // 生成RSA加密的key KeyPair keyPair = KeyGeneratorUtils.generateRsaKey(); // 公钥 RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); // 私钥 RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); // 构建RSA加密key return new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); } /** * 生成EC加密key (即JWK) */ public static ECKey generateEc() { // 生成EC加密的key KeyPair keyPair = KeyGeneratorUtils.generateEcKey(); // 公钥 ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic(); // 私钥 ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate(); // 根据公钥参数生成曲线 Curve curve = Curve.forECParameterSpec(publicKey.getParams()); // 构建EC加密key return new ECKey.Builder(curve, publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); } /** * 生成HmacSha256密钥 */ public static OctetSequenceKey generateSecret() { SecretKey secretKey = KeyGeneratorUtils.generateSecretKey(); return new OctetSequenceKey.Builder(secretKey) .keyID(UUID.randomUUID().toString()) .build(); } } class KeyGeneratorUtils { private KeyGeneratorUtils() { } /** * 生成RSA密钥 */ 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; } /** * 生成EC密钥 */ static KeyPair generateEcKey() { EllipticCurve ellipticCurve = new EllipticCurve( new ECFieldFp( new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951")), new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"), new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291")); ECPoint ecPoint = new ECPoint( new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"), new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109")); ECParameterSpec ecParameterSpec = new ECParameterSpec( ellipticCurve, ecPoint, new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"), 1); KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); keyPairGenerator.initialize(ecParameterSpec); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } return keyPair; } /** * 生成HmacSha256密钥 */ static SecretKey generateSecretKey() { SecretKey hmacKey; try { hmacKey = KeyGenerator.getInstance("HmacSha256").generateKey(); } catch (Exception ex) { throw new IllegalStateException(ex); } return hmacKey; } }
新建 ConsentController
,编写登录和认证页面的跳转
如果在上面没有使用自定义的登录和授权页面,下面的跳转方法按需舍去
@Slf4j @Controller public class ConsentController { private final RegisteredClientRepository registeredClientRepository; private final OAuth2AuthorizationConsentService authorizationConsentService; public ConsentController(RegisteredClientRepository registeredClientRepository, OAuth2AuthorizationConsentService authorizationConsentService) { this.registeredClientRepository = registeredClientRepository; this.authorizationConsentService = authorizationConsentService; } @ResponseBody @GetMapping("/favicon.ico") public String faviconico(){ return "favicon.ico"; } @GetMapping("/login") public String loginPage(){ return "login"; } @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) { // Remove scopes that were already approved Set scopesToApprove = new HashSet<>(); Set previouslyApprovedScopes = new HashSet<>(); 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 (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 = "UNKNOWN SCOPE - We cannot provide information about this permission, use caution when granting this."; private static final Map scopeDescriptions = new HashMap<>(); static { scopeDescriptions.put( "message.read", "This application will be able to read your message." ); scopeDescriptions.put( "message.write", "This application will be able to add new messages. It will also be able to edit and delete existing messages." ); scopeDescriptions.put( "other.scope", "This is another scope example of a scope description." ); } public final String scope; public final String description; ScopeWithDescription(String scope) { this.scope = scope; this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION); } } }
新建 UserController
,User,UserService等标准的自定义用户业务,此处仅放出UserServiceImpl
@RequiredArgsConstructor @Slf4j @Component class UserServiceImpl implements UserService { private final UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.selectOne(new LambdaQueryWrapper().eq(User::getUsername,username)); return new org.springframework.security.core.userdetails.User(username, user.getPassword(), new ArrayList<>()); } }
启动项目,如下图
认证服务器整体结构图
资源服务器相关配置
pom.xml引入资源服务器相关依赖
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-oauth2-resource-serverartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-securityartifactId> dependency>
新增配置文件 application.yaml
server: port: 9003 spring: application: name: resource security: oauth2: resourceserver: jwt: issuer-uri: http://127.0.0.1:9000 feign: client: config: default: #配置超时时间 connect-timeout: 10000 read-timeout: 10000
新增资源服务器配置文件 ResourceServerConfiguration
@Configuration @EnableWebSecurity(debug = true) @EnableGlobalMethodSecurity(prePostEnabled = true) //开启鉴权服务 public class ResourceServerConfiguration { @Bean public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception { // 所有请求都进行拦截 httpSecurity.authorizeRequests().anyRequest().authenticated(); // 关闭session httpSecurity.sessionManagement().disable(); // 配置资源服务器的无权限,无认证拦截器等 以及JWT验证 httpSecurity.oauth2ResourceServer() .accessDeniedHandler(new SimpleAccessDeniedHandler()) .authenticationEntryPoint(new SimpleAuthenticationEntryPoint()) .jwt(); return httpSecurity.build(); } }
新增相关无认证无权限统一拦截回复 SimpleAccessDeniedHandler
和 SimpleAuthenticationEntryPoint
/** * 携带了token 而且token合法 但是权限不足以访问其请求的资源 403 * @author zxg */ public class SimpleAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setCharacterEncoding("utf-8"); response.setContentType(MediaType.APPLICATION_JSON_VALUE); ObjectMapper objectMapper = new ObjectMapper(); String resBody = objectMapper.writeValueAsString(SingleResultBundle.failed("无权访问")); PrintWriter printWriter = response.getWriter(); printWriter.print(resBody); printWriter.flush(); printWriter.close(); } } /** * 在资源服务器中 不携带token 或者token无效 401 * @author zxg */ @Slf4j public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { if (response.isCommitted()){ return; } Throwable throwable = authException.fillInStackTrace(); String errorMessage = "认证失败"; if (throwable instanceof BadCredentialsException){ errorMessage = "错误的客户端信息"; }else { Throwable cause = authException.getCause(); if (cause instanceof JwtValidationException) { log.warn("JWT Token 过期,具体内容:" + cause.getMessage()); errorMessage = "无效的token信息"; } else if (cause instanceof BadJwtException){ log.warn("JWT 签名异常,具体内容:" + cause.getMessage()); errorMessage = "无效的token信息"; } else if (cause instanceof AccountExpiredException){ errorMessage = "账户已过期"; } else if (cause instanceof LockedException){ errorMessage = "账户已被锁定"; // } else if (cause instanceof InvalidClientException || cause instanceof BadClientCredentialsException){ // response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed(401,"无效的客户端"))); // } else if (cause instanceof InvalidGrantException || cause instanceof RedirectMismatchException){ // response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("无效的类型"))); // } else if (cause instanceof UnauthorizedClientException) { // response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("未经授权的客户端"))); } else if (throwable instanceof InsufficientAuthenticationException) { String message = throwable.getMessage(); if (message.contains("Invalid token does not contain resource id")){ errorMessage = "未经授权的资源服务器"; }else if (message.contains("Full authentication is required to access this resource")){ errorMessage = "缺少验证信息"; } }else { errorMessage = "验证异常"; } } response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setCharacterEncoding("utf-8"); response.setContentType(MediaType.APPLICATION_JSON_VALUE); ObjectMapper objectMapper = new ObjectMapper(); String resBody = objectMapper.writeValueAsString(SingleResultBundle.failed(errorMessage)); PrintWriter printWriter = response.getWriter(); printWriter.print(resBody); printWriter.flush(); printWriter.close(); } }
新增 ResourceController
进行接口测试
@Slf4j @RestController public class ResourceController { /** * 测试Spring Authorization Server,测试权限 */ @PreAuthorize("hasAuthority('SCOPE_message.read')") @GetMapping("/getTest") public String getTest(){ return "getTest"; } /** * 默认登录成功跳转页为 / 防止404状态 * * @return the map */ @GetMapping("/") public Map index() { return Collections.singletonMap("msg", "login success!"); } @GetMapping("/getResourceTest") public SingleResultBundle getResourceTest(){ return SingleResultBundle.success("这是resource的测试方法 getResourceTest()"); } }
启动项目,效果如下
项目总体结构如下
测试认证鉴权
#调用 /oauth2/authorize ,获取code http://127.0.0.1:9000/oauth2/authorize?client_id=zxg&response_type=code&scope=message.read&redirect_uri=http://www.baidu.com #会判断是否登录,若没有,则跳转到登录页面,如下图1 #登录完成后,会提示是否授权,若没有,则跳转到授权界面,如下图2 #授权成功后,跳转到回调地址,并带上code,如图3
打开postman,进行获取access_token
#访问 /oauth2/token 地址 #在Authorization中选择Basic Auth模式,填入对应客户端,其会在header中生成Authorization,如下图右侧
返回结果如下
调用ResourceController
中的接口,测试token是否生效
源码下载地址
- https://gitee.com/rjj521/authorization-server-learn
总结
至此,spring-authorization-server
的基础使用已完成,总体上和原Spring Security OAuth
大差不差,个别配置项不同。期间在网上搜寻了很多资料,然后进行整合,因此文中存在与其他网上教程相同代码,如有争议,请联系我删除改正,谢谢。
来源:blog.csdn.net/qq_37182370/article/ details/124822587