1. 前言
1.1?为什么使用spring-authorization-server?
????????真实原因:原先是因为个人原因,需要研究新版鉴权服务,看到了spring-authorization-server,使用过程中,想着能不能整合新版本cloud,因此此处先以springboot搭建spring-authorization-server,后续再替换为springcloud。
? ? ? ? 官方原因:原先使用Spring Security OAuth,而该项目已经逐渐被淘汰,虽然网上还是有不少该方案,但秉着技术要随时代更新,从而使用spring-authorization-server
2.项目迭代历程
-
以springboot搭建spring-authorization-server(即认证与资源服务器) -
升级为springcloud2021.0.x框架,引入nacos作为注册中心 -
引入sentinel,进行限流降级熔断配置 -
引入nacos配置中心 -
升级sentinel,配合nacos,使项目能动态配置落地所有Feign的降级配置 -
引入gateway网关,swagger文档工具 -
待续
3.项目构建
????????
3.1 以springboot搭建spring-authorization-server(即认证与资源服务器)
? ? ? ? 先进行认证服务器相关配置
? ? ? ? pom.xml引入依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<!-- 此依赖是个人公共依赖,你们引入其他具体依赖即可 -->
<dependency>
<groupId>com.xxxx.iov</groupId>
<artifactId>iov-cloud-framework-web</artifactId>
<version>2.0.0-SNAPSHOT</version>
<exclusions>
<!-- 这里是因为公共依赖中的web版本太低,所以移除 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.6</version>
</dependency>
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.0</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.39</version>
</dependency>
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- oauth2-authorization-server -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.2.3</version>
</dependency>
<!-- security-cas -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
<!-- thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 数据连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.9</version>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
? ? ? ? 创建自定义登录页面 login.html (可不要,使用自带的登录界面)
<!DOCTYPE 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 Page</title>
<style>
.is-invalid {
color: red;
}
.invalid-feedback {
color: red;
}
.mb-3 {
margin-bottom: 3px;
}
</style>
<script th:inline="javascript">
/*<![CDATA[*/
// 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 session</p>
<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 In</button>
</div>
</div>
</form>
<p class="mb-1">
<a href="javascript:void(0)">I forgot my password</a>
</p>
<p class="mb-0">
<a href="javascript:void(0)" class="text-center">Register a new membership</a>
</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(可不要,可使用自带的授权页面)
<!DOCTYPE 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<HttpSecurity> 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<SecurityContext> 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<String> 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<String> scopesToApprove = new HashSet<>();
Set<String> previouslyApprovedScopes = new HashSet<>();
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
OAuth2AuthorizationConsent currentAuthorizationConsent =
this.authorizationConsentService.findById(registeredClient.getId(), principal.getName());
Set<String> 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<ScopeWithDescription> withDescription(Set<String> scopes) {
Set<ScopeWithDescription> 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<String, String> 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<User>().eq(User::getUsername,username));
return new org.springframework.security.core.userdetails.User(username, user.getPassword(), new ArrayList<>());
}
}
????????启动项目,如下图
????????认证服务器整体结构图
----------------------------------------------------------------------------------------------------------------------------------------------------------
????????资源服务器相关配置
????????pom.xml引入资源服务器相关依赖
<!-- resource-server资源服务器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</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<String, String> index() {
return Collections.singletonMap("msg", "login success!");
}
@GetMapping("/getResourceTest")
public SingleResultBundle<String> 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是否生效
总结
至此,spring-authorization-server的基础使用已完成,总体上和原Spring Security OAuth大差不差,个别配置项不同。期间在网上搜寻了很多资料,然后进行整合,因此文中存在与其他网上教程相同代码,如有争议,请联系我删除改正,谢谢。由于不太会写文章,我就直接贴出代码,代码中我有加上注释,所以上述文章中,没有很具体的描述,基本就是个人开发流程,若文中有那里写不对,欢迎指教,不喜勿喷。
关于升级springcloud2021.0.x部分,请查看后续相关文章
|