前言
之前写了认证服务,实际生产中都是前后端分离的项目,现在来搭建客户端和资源服务器。
代码
客户端代码
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("error_uri", error.getUri());
}
if (StringUtils.hasText(error.getDescription())) {
parameters.put("error_description", error.getDescription());
}
if (error instanceof BearerTokenError bearerTokenError) {
if (StringUtils.hasText(bearerTokenError.getScope())) {
parameters.put("scope", bearerTokenError.getScope());
}
response.setStatus(bearerTokenError.getHttpStatus().value());
}
}
if (e instanceof InsufficientAuthenticationException) {
// 没有携带jwt访问接口,没有客户端认证信息
parameters.put("error", BearerTokenErrorCodes.INVALID_TOKEN);
parameters.put("error_description", "Not authorized.");
parameters.put("error_uri", "https://tools.ietf.org/html/rfc6750#section-3.1");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
parameters.put("message", e.getMessage());
return parameters;
}
/**
* 生成放入请求头的错误信息
*
* @param parameters 参数
* @return 字符串
*/
public static String computeWwwAuthenticateHeaderValue(Map<String, String> parameters) {
StringBuilder wwwAuthenticate = new StringBuilder();
wwwAuthenticate.append("Bearer");
if (!parameters.isEmpty()) {
wwwAuthenticate.append(" ");
int i = 0;
for (Map.Entry<String, String> entry : parameters.entrySet()) {
wwwAuthenticate.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\"");
if (i != parameters.size() - 1) {
wwwAuthenticate.append(", ");
}
i++;
}
}
return wwwAuthenticate.toString();
}
}
3. JsonUtils
@Slf4j
public class JsonUtils {
private JsonUtils() {
// 禁止实例化工具类
throw new UnsupportedOperationException("Utility classes cannot be instantiated.");
}
private final static ObjectMapper MAPPER = new ObjectMapper();
static {
// 对象的所有字段全部列入,还是其他的选项,可以忽略null等
MAPPER.setSerializationInclusion(JsonInclude.Include.ALWAYS);
// 取消默认的时间转换为timeStamp格式
MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
// 设置Date类型的序列化及反序列化格式
MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
// 忽略空Bean转json的错误
MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// 忽略未知属性,防止json字符串中存在,java对象中不存在对应属性的情况出现错误
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/**
* json字符串转为对象
*
* @param json json
* @param clazz T类的class文件
* @param <T> 泛型, 代表返回参数的类型
* @return 返回T的实例
*/
public static <T> T jsonCovertToObject(String json, Class<T> clazz) {
if (json == null || clazz == null) {
return null;
}
try {
return MAPPER.readValue(json, clazz);
} catch (IOException e) {
log.error("json转换失败,原因:", e);
}
return null;
}
/**
* json字符串转为对象
*
* @param json json
* @param type 对象在Jackson中的类型
* @param <T> 泛型, 代表返回参数的类型
* @return 返回T的实例
*/
public static <T> T jsonCovertToObject(String json, TypeReference<T> type) {
if (json == null || type == null) {
return null;
}
try {
return MAPPER.readValue(json, type);
} catch (IOException e) {
log.error("json转换失败,原因:", e);
}
return null;
}
/**
* 将流中的数据转为java对象
*
* @param inputStream 输入流
* @param clazz 类的class
* @param <T> 泛型, 代表返回参数的类型
* @return 返回对象 如果参数任意一个为 null则返回null
*/
public static <T> T covertStreamToObject(InputStream inputStream, Class<T> clazz) {
if (inputStream == null || clazz == null) {
return null;
}
try {
return MAPPER.readValue(inputStream, clazz);
} catch (IOException e) {
log.error("json转换失败,原因:", e);
}
return null;
}
/**
* json字符串转为复杂类型List
*
* @param json json
* @param collectionClazz 集合的class
* @param elementsClazz 集合中泛型的class
* @param <T> 泛型, 代表返回参数的类型
* @return 返回T的实例
*/
public static <T> T jsonCovertToObject(String json, Class<?> collectionClazz, Class<?>... elementsClazz) {
if (json == null || collectionClazz == null || elementsClazz == null) {
return null;
}
try {
JavaType javaType = MAPPER.getTypeFactory().constructParametricType(collectionClazz, elementsClazz);
return MAPPER.readValue(json, javaType);
} catch (IOException e) {
log.error("json转换失败,原因:", e);
}
return null;
}
/**
* 对象转为json字符串
*
* @param o 将要转化的对象
* @return 返回json字符串
*/
public static String objectCovertToJson(Object o) {
if (o == null) {
return null;
}
try {
return o instanceof String ? (String) o : MAPPER.writeValueAsString(o);
} catch (IOException e) {
log.error("json转换失败,原因:", e);
}
return null;
}
/**
* 将对象转为另一个对象
* 切记,两个对象结构要一致
* 多用于Object转为具体的对象
*
* @param o 将要转化的对象
* @param collectionClazz 集合的class
* @param elementsClazz 集合中泛型的class
* @param <T> 泛型, 代表返回参数的类型
* @return 返回T的实例
*/
public static <T> T objectCovertToObject(Object o, Class<?> collectionClazz, Class<?>... elementsClazz) {
String json = objectCovertToJson(o);
return jsonCovertToObject(json, collectionClazz, elementsClazz);
}
}
4. TestController
@RestController
public class TestController {
@GetMapping("/test01")
@PreAuthorize("hasAuthority('SCOPE_message.read')")
public String test01() {
return "test01";
}
@GetMapping("/test02")
@PreAuthorize("hasAuthority('SCOPE_message.write')")
public String test02() {
return "test02";
}
@GetMapping("/app")
@PreAuthorize("hasAuthority('app')")
public String app() {
return "app";
}
}
5. application.yml
server:
# 设置资源服务器端口
port: 8001
spring:
security:
oauth2:
# 资源服务器配置
resourceserver:
jwt:
# Jwt中claims的iss属性,也就是jwt的签发地址,即认证服务器的根路径
# 资源服务器会进一步的配置,通过该地址获取公钥以解析jwt
issuer-uri: http://c.example.com:8080
6. 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-resource-server</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>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
资源服务器要配置认证服务器的地址,当客户端携带token访问资源服务器会调用认证服务器解析token
认证服务代码
1. 修改AuthorizationConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class AuthorizationConfig {
private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";
private final RedisSecurityContextRepository redisSecurityContextRepository;
private final RedisOperator<String> redisOperator;
/**
* 生成rsa密钥对,提供给jwk
*
* @return 密钥对
*/
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;
}
/**
* 配置端点的过滤器链
*
* @param http spring security核心配置类
* @return 过滤器链
* @throws Exception 抛出
*/
@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http
, RegisteredClientRepository registeredClientRepository
, AuthorizationServerSettings authorizationServerSettings) throws Exception {
// OAuth2 security配置 默认的设置,忽略认证端点的csrf校验
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// 新建设备码converter和provider
DeviceClientAuthenticationConverter deviceClientAuthenticationConverter =
new DeviceClientAuthenticationConverter(
authorizationServerSettings.getDeviceAuthorizationEndpoint());
DeviceClientAuthenticationProvider deviceClientAuthenticationProvider =
new DeviceClientAuthenticationProvider(registeredClientRepository);
// 使用redis存储、读取登录的认证信息
http.securityContext(context -> context.securityContextRepository(redisSecurityContextRepository));
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
// 开启OpenID Connect 1.0协议相关端点
.oidc(Customizer.withDefaults())
// 设置自定义用户确认授权页
.authorizationEndpoint(authorizationEndpoint -> {
System.out.println("process trace | authorizationEndpoint.consentPage(" + CUSTOM_CONSENT_PAGE_URI + ")");
authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI);
})
// 设置设备码用户验证url(自定义用户验证页)
.deviceAuthorizationEndpoint(deviceAuthorizationEndpoint -> {
deviceAuthorizationEndpoint.verificationUri("/activate");
})
// 设置验证设备码用户确认页面
.deviceVerificationEndpoint(deviceVerificationEndpoint -> {
deviceVerificationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI);
})
// 客户端认证添加设备码的converter和provider
.clientAuthentication(clientAuthentication -> {
clientAuthentication
.authenticationConverter(deviceClientAuthenticationConverter)
.authenticationProvider(deviceClientAuthenticationProvider);
});
http
// 当未登录时访问认证端点时重定向至login页面
.exceptionHandling((exceptions) -> {
System.out.println("process trace | exceptions.defaultAuthenticationEntryPointFor(/login)");
// 下面是旧的处理,上面是新的处理 使用自定义的未登录处理,并设置登录地址为前端的登录地址
exceptions.defaultAuthenticationEntryPointFor(
new LoginTargetAuthenticationEntryPoint("http://127.0.0.1:3000")
, new MediaTypeRequestMatcher(MediaType.TEXT_HTML));
/*exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login")
, new MediaTypeRequestMatcher(MediaType.TEXT_HTML));*/
})
// 处理使用access token访问用户信息端点和客户端注册端点
.oauth2ResourceServer((resourceServer) -> {
System.out.println("process trace | resourceServer.jwt(Customizer.withDefaults())");
resourceServer.jwt(Customizer.withDefaults());
});
// 自定义短信认证登录转换器
SmsCaptchaGrantAuthenticationConverter converter = new SmsCaptchaGrantAuthenticationConverter();
// 自定义短信认证登录认证提供
SmsCaptchaGrantAuthenticationProvider provider = new SmsCaptchaGrantAuthenticationProvider();
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
// 让认证服务器元数据中有自定义的认证方式
.authorizationServerMetadataEndpoint(metadata -> {
metadata.authorizationServerMetadataCustomizer(customizer -> {
customizer.grantType(SecurityConstants.GRANT_TYPE_SMS_CODE);
});
})
// 添加自定义grant_type——短信认证登录
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
.accessTokenRequestConverter(converter)
.authenticationProvider(provider));
DefaultSecurityFilterChain build = http.build();
// 从框架中获取provider中所需的bean
OAuth2TokenGenerator<?> tokenGenerator = http.getSharedObject(OAuth2TokenGenerator.class);
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class);
// 以上三个bean在build()方法之后调用是因为调用build方法时框架会尝试获取这些类,
// 如果获取不到则初始化一个实例放入SharedObject中,所以要在build方法调用之后获取
// 在通过set方法设置进provider中,但是如果在build方法之后调用authenticationProvider(provider)
// 框架会提示unsupported_grant_type,因为已经初始化完了,在添加就不会生效了
provider.setTokenGenerator(tokenGenerator);
provider.setAuthorizationService(authorizationService);
provider.setAuthenticationManager(authenticationManager);
return build;
}
/**
* 配置认证相关的过滤器链
*
* @param http spring security核心配置类
* @return 过滤器链
* @throws Exception 抛出
*/
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// 添加跨域过滤器
http.addFilter(corsFilter());
// 禁用 csrf 与 cors
http.csrf(AbstractHttpConfigurer::disable);
http.cors(AbstractHttpConfigurer::disable);
http.authorizeHttpRequests((authorize) -> {
System.out.println("process trace | authorizeHttpRequests config");
authorize
.requestMatchers("/assets/**", "/webjars/**", "/login", "/getCaptcha"
, "/getSmsCaptcha").permitAll()
.anyRequest().authenticated();
})
// 指定登录页面 这里一定不能注释掉 不然页面请求不到post的login
.formLogin(formLogin -> {
System.out.println("process trace | formLogin.loginPage(\"/login\")");
formLogin.loginPage("/login")
// 登录成功和失败改为写回json,不重定向了
.successHandler(new LoginSuccessHandler())
.failureHandler(new LoginFailureHandler());
});
// 添加BearerTokenAuthenticationFilter,将认证服务当做一个资源服务,解析请求头中的token
http.oauth2ResourceServer((resourceServer) -> {
resourceServer
.jwt(Customizer.withDefaults())
.accessDeniedHandler(SecurityUtils::exceptionHandler)
.authenticationEntryPoint(SecurityUtils::exceptionHandler);
});
http
// 当未登录时访问认证端点时重定向至login页面
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginTargetAuthenticationEntryPoint("http://127.0.0.1:3000"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
);
// 使用redis存储、读取登录的认证信息
http.securityContext(context -> context.securityContextRepository(redisSecurityContextRepository));
// 在UsernamePasswordAuthenticationFilter拦截器之前添加验证码校验拦截器,并拦截POST的登录接口
// http.addFilterBefore(new CaptchaAuthenticationFilter("/login"), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
/**
* 跨域过滤器配置
*
* @return CorsFilter
*/
@Bean
public CorsFilter corsFilter() {
// 初始化cors配置对象
CorsConfiguration configuration = new CorsConfiguration();
// 设置跨域访问可以携带cookie 出现该问题是跨域问题,当allowCredentials为true时,allowingOrigins不能包含特殊值“ *”,
// 因为无法在“ Access-Control-Allow-Origin”响应标头上设置。
//处理方法是:首先找打你后端跨域设置文件,将方法中的addAllowedOrigin(““)替换成addAllowedOriginPattern(””)
configuration.setAllowCredentials(true);
// 设置允许跨域的域名,如果允许携带cookie的话,路径就不能写*号, *表示所有的域名都可以跨域访问
configuration.addAllowedOriginPattern("http://127.0.0.1:8080");
configuration.addAllowedOriginPattern("http://c.example.com:8080");
configuration.addAllowedOriginPattern("http://127.0.0.1:3000");
// 允许所有的请求方法 ==> GET POST PUT Delete
configuration.addAllowedMethod("*");
// 允许携带任何头信息
configuration.addAllowedHeader("*");
// 初始化cors配置源对象
UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource();
// 给配置源对象设置过滤的参数
// 参数一: 过滤的路径 == > 所有的路径都要求校验是否跨域
// 参数二: 配置类
configurationSource.registerCorsConfiguration("/**", configuration);
// 返回配置好的过滤器
return new CorsFilter(configurationSource);
}
/**
* 自定义jwt,将权限信息放至jwt中
*
* @return OAuth2TokenCustomizer的实例
*/
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> oAuth2TokenCustomizer() {
return context -> {
// 检查登录用户信息是不是UserDetails,排除掉没有用户参与的流程
if (context.getPrincipal().getPrincipal() instanceof UserDetails user) {
// 获取申请的scopes
Set<String> scopes = context.getAuthorizedScopes();
// 获取用户的权限
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
// 提取权限并转为字符串
Set<String> authoritySet = Optional.ofNullable(authorities).orElse(Collections.emptyList()).stream()
// 获取权限字符串
.map(GrantedAuthority::getAuthority)
// 去重
.collect(Collectors.toSet());
// 合并scope与用户信息
authoritySet.addAll(scopes);
JwtClaimsSet.Builder claims = context.getClaims();
// 将权限信息放入jwt的claims中(也可以生成一个以指定字符分割的字符串放入)
claims.claim("authorities", authoritySet);
// 放入其它自定内容
// 角色、头像...
}
};
}
/**
* 自定义jwt解析器,设置解析出来的权限信息的前缀与在jwt中的key
*
* @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;
}
/**
* 配置密码解析器,使用BCrypt的方式对密码进行加密和验证
*
* @return BCryptPasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置客户端Repository
*
* @param jdbcTemplate db 数据源信息
* @param passwordEncoder 密码解析器
* @return 基于数据库的repository
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate
, PasswordEncoder passwordEncoder) {
System.out.println("password encode : " + passwordEncoder.encode("123456"));
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
// 客户端id
.clientId("messaging-client")
// 客户端秘钥,使用密码解析器加密
.clientSecret(passwordEncoder.encode("123456"))
// 客户端认证方式,基于请求头的认证
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 配置资源服务器使用该客户端获取授权时支持的方式
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// .authorizationGrantType(SecurityConstants.GRANT_TYPE_SMS_CODE)
// 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost,本机使用127.0.0.1访问
// 这里每次启动是不会该数据库的,所以改了这个链接要相应的改一下数据库
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
.redirectUri("https://www.baidu.com/")
.redirectUri("http://127.0.0.1:8080/notify/oauth2/code")
// 该客户端的授权范围,OPENID与PROFILE是IdToken的scope,获取授权时请求OPENID的scope时认证服务会返回IdToken
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
// 自定scope
.scope("message.read")
.scope("message.write")
// 客户端设置,设置用户需要确认授权
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
// 基于db存储客户端,还有一个基于内存的实现 InMemoryRegisteredClientRepository
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
// 初始化客户端
RegisteredClient repositoryByClientId =
registeredClientRepository.findByClientId(registeredClient.getClientId());
if (repositoryByClientId == null) {
registeredClientRepository.save(registeredClient);
}
// 设备码授权客户端
RegisteredClient deviceClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("device-message-client")
// 公共客户端
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
// 设备码授权
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// 自定scope
.scope("message.read")
.scope("message.write")
.build();
RegisteredClient byClientId = registeredClientRepository.findByClientId(deviceClient.getClientId());
if (byClientId == null) {
registeredClientRepository.save(deviceClient);
}
// PKCE客户端
RegisteredClient pkceClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("pkce-message-client")
// 公共客户端
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
// 设备码授权
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost,本机使用127.0.0.1访问
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
.clientSettings(ClientSettings.builder().requireProofKey(Boolean.TRUE).build())
// 自定scope
.scope("message.read")
.scope("message.write")
.build();
RegisteredClient findPkceClient = registeredClientRepository.findByClientId(pkceClient.getClientId());
if (findPkceClient == null) {
registeredClientRepository.save(pkceClient);
}
return registeredClientRepository;
}
/**
* 配置基于db的oauth2的授权管理服务
*
* @param jdbcTemplate db数据源信息
* @param registeredClientRepository 上边注入的客户端repository
* @return JdbcOAuth2AuthorizationService
*/
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
// 基于db的oauth2认证服务,还有一个基于内存的服务实现InMemoryOAuth2AuthorizationService
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
/**
* 配置基于db的授权确认管理服务
*
* @param jdbcTemplate db数据源信息
* @param registeredClientRepository 客户端repository
* @return JdbcOAuth2AuthorizationConsentService
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
// 基于db的授权确认管理服务,还有一个基于内存的服务实现InMemoryOAuth2AuthorizationConsentService
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
/**
* 配置jwk源,使用非对称加密,公开用于检索匹配指定选择器的JWK的方法
* 在配置JWKSource之前先从redis中尝试获取一下,获取不到就生成并存入redis;获取到直接解析并生成一个JWKSet
*
* @return JWKSource
*/
@Bean
@SneakyThrows
public JWKSource<SecurityContext> jwkSource() {
// 先从redis获取
String jwkSetCache = redisOperator.get(RedisConstants.AUTHORIZATION_JWS_PREFIX_KEY);
if (StringUtils.isEmpty(jwkSetCache)) {
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();
// 生成jws
JWKSet jwkSet = new JWKSet(rsaKey);
// 转为json字符串
String jwkSetString = jwkSet.toString(Boolean.FALSE);
// 存入redis
redisOperator.set(RedisConstants.AUTHORIZATION_JWS_PREFIX_KEY, jwkSetString);
return new ImmutableJWKSet<>(jwkSet);
}
// 解析存储的jws
JWKSet jwkSet = JWKSet.parse(jwkSetCache);
return new ImmutableJWKSet<>(jwkSet);
}
/**
* 配置jwt解析器
*
* @param jwkSource jwk源
* @return JwtDecoder
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* 添加认证服务器配置,设置jwt签发者、默认端点请求地址等
*
* @return AuthorizationServerSettings
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
/*
设置token签发地址(http(s)://{ip}:{port}/context-path, http(s)://domain.com/context-path)
如果需要通过ip访问这里就是ip,如果是有域名映射就填域名,通过什么方式访问该服务这里就填什么
签发信息是调用资源服务器时能够使资源服务器识别的
*/
.issuer("http://c.example.com:8080")
.build();
}
/**
* 先暂时配置一个基于内存的用户,框架在用户认证时会默认调用
* {@link UserDetailsService#loadUserByUsername(String)} 方法根据
* 账号查询用户信息,一般是重写该方法实现自己的逻辑
*
* @param passwordEncoder 密码解析器
* @return UserDetailsService
*/
/*@Bean
public UserDetailsService users(PasswordEncoder passwordEncoder) {
UserDetails user = User.withUsername("admin1")
.password(passwordEncoder.encode("123456"))
.roles("admin", "normal", "unAuthentication")
.authorities("app", "web", "/test2", "/test3")
.build();
return new InMemoryUserDetailsManager(user);
}*/
}
- 去掉了内存获取用户信息改为mysql
- 修改签发地址
- 新增登录成功失败处理器
- session存入redis
- 自定义跳转前端登录页面(前后端分离)
- 新增跨域Filter
- 新增模拟短信登录
内容比较多,比较复杂,需要好好理解梳理!!!
测试
访问http://127.0.0.1:8000/app(客户端接口)跳转到认证登录页面
这个时候服务器不会跳转,因为现在有了成功失败处理器都只会返回json,需浏览器再调一下记得携带cookie
剩下的晚点更新!!!!