前言
之前写了认证服务,实际生产中都是前后端分离的项目,现在来搭建客户端和资源服务器。
代码
客户端代码
1. 新建module
2. 添加TestController
/**
* 测试接口
*
* @author lxq
*/
@RestController
public class TestController {
@GetMapping("/test01")
@PreAuthorize("hasAuthority('message.read')")
public String test01() {
Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
return "test01";
}
@GetMapping("/test02")
@PreAuthorize("hasAuthority('SCOPE_message.write')")
public String test02() {
return "test02";
}
@GetMapping("/app")
@PreAuthorize("hasAuthority('app')")
public String app() {
return "app";
}
}
2. 添加application.yml
server:
# 修改端口
port: 8000
spring:
security:
oauth2:
client:
provider:
# 认证提供者,自定义名称
custom-issuer:
# Token签发地址(认证服务地址)
issuer-uri: http://c.example.com:8080
# 获取用户信息的地址,默认的/userinfo端点需要IdToken获取,为避免麻烦自定一个用户信息接口
user-info-uri: ${spring.security.oauth2.client.provider.custom-issuer.issuer-uri}/user
registration:
messaging-client-oidc:
# oauth认证提供者配置,和上边配置的认证提供者关联起来
provider: custom-issuer
# 客户端名称,自定义
client-name: message-client
# 客户端id,从认证服务申请的客户端id
client-id: messaging-client
# 客户端秘钥
client-secret: 123456
# 客户端认证方式
client-authentication-method: client_secret_basic
# 获取Token使用的授权流程
authorization-grant-type: authorization_code
# 回调地址,这里设置为Spring Security Client默认实现使用code换取token的接口
redirect-uri: http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc
scope:
- message.read
- message.write
3. pom.xml
<dependencies>
<!-- 引入bootstrap依赖,不引入这个依赖是无法使用bootstrap配置文件的 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
客户端的代码相对比较简单,spring boot 帮我们封装好了。
资源端代码
1. ResourceServerConfig
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class ResourceServerConfig {
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> {
// 下边一行是放行接口的配置,被放行的接口上不能有权限注解,e.g. @PreAuthorize,否则无效
// .requestMatchers("/test02").permitAll()
authorize.anyRequest().authenticated();
})
.oauth2ResourceServer(oauth2 -> {
oauth2
// 可在此处添加自定义解析设置
.jwt(Customizer.withDefaults())
// 添加未携带token和权限不足异常处理
.accessDeniedHandler(SecurityUtils::exceptionHandler)
.authenticationEntryPoint(SecurityUtils::exceptionHandler);
});
return http.build();
}
/**
* 自定义jwt解析器,设置解析出来的权限信息的前缀与在jwt中的key
* 添加自定义解析token配置,注入一个JwtAuthenticationConverter
*
* @return jwt解析器 JwtAuthenticationConverter
*/
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
// 设置解析权限信息的前缀,设置为空是去掉前缀
grantedAuthoritiesConverter.setAuthorityPrefix("");
// 设置权限信息在jwt claims中的key
grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
2. SecurityUtils
public class SecurityUtils {
private SecurityUtils() {
// 禁止实例化工具类
throw new UnsupportedOperationException("Utility classes cannot be instantiated.");
}
/**
* 认证与鉴权失败回调
*
* @param request 当前请求
* @param response 当前响应
* @param e 具体的异常信息
*/
public static void exceptionHandler(HttpServletRequest request, HttpServletResponse response, Throwable e) {
Map<String, String> parameters = getErrorParameter(request, response, e);
String wwwAuthenticate = computeWwwAuthenticateHeaderValue(parameters);
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
try {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(JsonUtils.objectCovertToJson(parameters));
response.getWriter().flush();
} catch (IOException ex) {
log.error("写回错误信息失败", e);
}
}
/**
* 获取异常信息map
*
* @param request 当前请求
* @param response 当前响应
* @param e 本次异常具体的异常实例
* @return 异常信息map
*/
private static Map<String, String> getErrorParameter(HttpServletRequest request, HttpServletResponse response, Throwable e) {
Map<String, String> parameters = new LinkedHashMap<>();
if (request.getUserPrincipal() instanceof AbstractOAuth2TokenAuthenticationToken) {
// 权限不足
parameters.put("error", BearerTokenErrorCodes.INSUFFICIENT_SCOPE);
parameters.put("error_description",
"The request requires higher privileges than provided by the access token.");
parameters.put("error_uri", "https://tools.ietf.org/html/rfc6750#section-3.1");
response.setStatus(HttpStatus.FORBIDDEN.value());
}
if (e instanceof OAuth2AuthenticationException authenticationException) {
// jwt异常,e.g. jwt超过有效期、jwt无效等
OAuth2Error error = authenticationException.getError();
parameters.put("error", error.getErrorCode());
if (StringUtils.hasText(error.getUri())) {
parameters.put("er