真实原因:原先是因为个人原因,需要研究新版鉴权服务,看到了spring-authorization-server
,使用过程中,想着能不能整合新版本cloud,因此此处先以springboot搭建spring-authorization-server
,后续再替换为springcloud2021。
官方原因:原先使用Spring Security OAuth
,而该项目已经逐渐被淘汰,虽然网上还是有不少该方案,但秉着技术要随时代更新,从而使用spring-authorization-server
Spring 团队正式宣布 Spring Security OAuth
停止维护,该项目将不会再进行任何的迭代
需要创建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">
/**/
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//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
大差不差,个别配置项不同。期间在网上搜寻了很多资料,然后进行整合,因此文中存在与其他网上教程相同代码,如有争议,请联系我删除改正,谢谢。
审核编辑 :李倩
全部0条评论
快来发表一下你的评论吧 !