SpringSecurityoauth2.0自整理文档
- 被标记为资源的url不会走用户认证过滤器,所以通过createBefor=>AuthFilter添加的过滤器无效
- 也就是在ResourceServerConfigurerAdapter实现类中配置的资源路径
- 记录一下手动加载用户和调用系统方法加载用户,以及他们的配置
- 记录一下自动加载用户和自动密码校验的配置
- 获取授权码的每个参数
- 获取token的每个参数链接
- 记录一下过滤器的处理
- 记录一下前后端分离和不分离的区别
- 记录如何添加过滤器到认证服务器前面
- 记录一下重定向地址携带参数的问题
基本流程
业务流程
- 使用用户身份申请授权码(此时需保证用户登录状态否则需要登录)
- 使用客户端身份+授权码申请accessToken
- 使用accessToken访问受保护资源
集成流程
- 配置资源所有者认证规则,继承WebSecurityConfigurerAdapter实现相关配置,配置资源的访问规则,设置哪些需要鉴权哪些不需要鉴权
- 配置资源所有者加载器,实现UserDetailsService,然后在步骤1的configure(AuthenticationManagerBuilder auth)中配置进去
- 如果使用的是基于jwt的登录状态保持,需要添加一个过滤器到UsernamePasswordAuthenticationFilter过滤器之前,然后在里面完成身份的验证,验证通过以后创建一个用户详情对象UserDetails存入线程域中,完成登录,后续的过滤器看到这个对象就认为他已经完成了认证
- 如果基于会话的,可以直接使用自带的密码校验工具,在登录时主动调用如下方法,届时就会自动调用UserDetailService.loadUserByUsername然后调用我们在第3条传递的密码校验规则进行密码比对
// 不为空再进行安全上下文的生成和赋予;如果为空直接放行,下一个过滤器会收拾他,不过不要修改加解密bean
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getId(), "success");
// 手动调用security的校验方法,会调用校验管理员,触发我们定义好的用户加载和加解密校验,传入经过处理的authenticationToken
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 将获得到的[用户安全上下文]对象设置到[安全上下文持有者]中
SecurityContextHolder.getContext().setAuthentication(authenticate);
- 配置资源所有者验证规则,重写WebSecurityConfigurerAdapter的configure(AuthenticationManagerBuilder auth)方法以配置用户校验规则
- 配置资源认证服务,继承AuthorizationServerConfigurerAdapter实现相关配置,设置资源的访问权限,例如资源id和资源scope等
- 如果配置使用jdbc加载,需要配置客户端加载器,实现ClientDetailsService重写loadClientByClientId方法,最后返回一个ClientDetails,当客户端来鉴权的时候,不再去内存找,而是调用这个方法
- 如果需要自定义客户端校验规则可以重写configure(AuthorizationServerSecurityConfigurer security)方法以修改
- 配置资源服务,继承ResourceServerConfigurerAdapter重写configure(HttpSecurity http)以配置资源的访问规则
- 自定义授权页面
总而言之就是要:
- 处理资源所有者的加载和验证,保证获取授权码的时候资源所有者是登录状态
- 处理客户端的加载和验证,保证使用授权码的兑换token的时候客户端是登录状态
- 处理资源的访问规则,
- 处理资源的验证规则,
依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>team.sss</groupId>
<artifactId>open-platform</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
</parent>
<properties>
<spring.cloud-version>Hoxton.SR8</spring.cloud-version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!--System-->
<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>
<version>2.3.3.RELEASE</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--Tool-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.7.9</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.20</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.8.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.3.RELEASE</version>
</plugin>
</plugins>
</build>
</project>
基于会话的Oauth2.0
-配置认证服务器
-配置资源服务器
基于Jwt的Oauth2.0
这里的jwt指的是用于维持资源所有者登录状态时使用jwt
-配置资源认证服务器
此配置管理资源的认证,用于配置资源的访问规则
基于内存加载客户端
把客户端写在内存
/**
* 授权服务器配置
*
* @author Guochao
*/
@Configuration
@EnableAuthorizationServer // 启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final PasswordEncoder passwordEncoder;
private final AdopApplicationService adopApplicationService;
private final CustomJwtTokenFilter customJwtTokenFilter;
public AuthorizationServerConfig(PasswordEncoder passwordEncoder, AdopApplicationService adopApplicationService, CustomJwtTokenFilter customJwtTokenFilter) {
this.passwordEncoder = passwordEncoder;
this.adopApplicationService = adopApplicationService;
this.customJwtTokenFilter = customJwtTokenFilter;
}
// 配置客户端
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 加载合作伙伴应用的信息(模板)
// clients.inMemory()
// .withClient("pzh") // clientId,客户端id
// // 客户端密码,客户端传输过来的密钥会进行加密,就用你注入进去的那个,所以如果你是明文就需要在这里进行加密以后写入,如果你数据库存的就是密文,则直接写入
// .secret(passwordEncoder.encode("123456"))
// // 重定向的地址,用户同意授权以后会携带授权码请求回调地址,从而获取授权码
// .redirectUris("http://localhost:9998/oauth/call-back")
// .scopes("resource", "userinfo", "all") // 授权允许的范围
// .authorizedGrantTypes("authorization_code", "refresh_token") // 授权类型,这里选择授权码模式
// .autoApprove(true) // 绝对自动授权,开启以后不用用户手动确认,不推荐,除非实在不想和用户交互
// ;
// 改为从数据库加载第三方平台信息,第三方接入量超过1W以后使用分页,小声bb:达到这个数量级有点难阿;
List<LoadThirdPartyPlatformsDto> thirdPartyPlatforms = adopApplicationService.getAllToLoadThirdPartyPlatformsDto();
// 获取内存写入对象,一定要在循环外创建,否则每次循环都是拿到一个新的,这样只有最后一个会生效
InMemoryClientDetailsServiceBuilder inMemory = clients.inMemory();
for (LoadThirdPartyPlatformsDto partyPlatform : thirdPartyPlatforms) {
ClientDetailsServiceBuilder<InMemoryClientDetailsServiceBuilder>.ClientBuilder builder = inMemory
.withClient(partyPlatform.getClientId().toString())
.secret(partyPlatform.getSecret())
.redirectUris(partyPlatform.getRedirectUri());
// 授权空间list
List<AdopScopeDto> scopes = partyPlatform.getScopes();
if (CollUtil.isNotEmpty(scopes)) {
builder.scopes(scopes.stream().map(AdopScope::getScopeCode).toArray(String[]::new))
.autoApprove(scopes.stream().filter(s -> s.getAutoStatus() == 1 && s.getId() != null).map(AdopScopeDto::getScopeCode).toArray(String[]::new));
}
// 授权类型list
List<AdopGrantType> grantTypes = partyPlatform.getGrantTypes();
if (CollUtil.isNotEmpty(grantTypes)) {
builder.authorizedGrantTypes(grantTypes.stream().filter(g -> g.getId() != null).map(AdopGrantType::getGrantTypeCode).toArray(String[]::new));
} else {
// 如果为空就默认授权码+刷新模式
builder.authorizedGrantTypes("authorization_code");
}
}
}
}
基于JDBC加载客户端
客户端认证时查询服务器获取结果
/**
* 授权服务器配置
*
* @author Guochao
*/
@Configuration
@EnableAuthorizationServer // 启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final PasswordEncoder passwordEncoder;
private final AdopApplicationService adopApplicationService;
private final ClientDetailServiceJDBCImpl jdbcClientDetailService;
public AuthorizationServerConfig(PasswordEncoder passwordEncoder, AdopApplicationService adopApplicationService, ClientDetailServiceJDBCImpl jdbcClientDetailService) {
this.passwordEncoder = passwordEncoder;
this.adopApplicationService = adopApplicationService;
this.jdbcClientDetailService = jdbcClientDetailService;
}
// 配置客户端
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 加载应用客户端的信息
// 配置客户端详情加载器
final ClientDetailsServiceBuilder<?> serviceBuilder = clients.withClientDetails(jdbcClientDetailService);
final JdbcClientDetailsServiceBuilder jdbc = serviceBuilder.jdbc();
// 配置加密解密
jdbc.passwordEncoder(passwordEncoder);
// 配置数据源
jdbc.dataSource(new DruidDataSource());
}
}
客户端详情加载器
实现org.springframework.security.oauth2.provider.ClientDetailsService;接口并重写他的loadClientByClientId接口,然后把这个对象注入到资源认证服务器配置中,并设置进withClientDetails中
这样在客户端验证的时候就会自动调用我们的实现方法,我们只需要在这里返回对应的ClientDetails就可以了
@Component
public class ClientDetailServiceJDBCImpl implements ClientDetailsService {
private final AdopApplicationService adopApplicationService;
public ClientDetailServiceJDBCImpl(AdopApplicationService adopApplicationService) {
this.adopApplicationService = adopApplicationService;
}
@Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
final LoadThirdPartyPlatformsDto appDto = Optional.ofNullable(adopApplicationService.getClientById(Long.valueOf(clientId)))
.orElseThrow(() -> new RuntimeException("ClientId not found"));
return cpToClientDetails(appDto);
}
public ClientDetails cpToClientDetails(LoadThirdPartyPlatformsDto adopApplication) {
// 实现将AdopApplication对象转换为ClientDetails对象的逻辑
return new CustomClientDetails(adopApplication);
}
}
客户端详情对象
实现org.springframework.security.oauth2.provider.ClientDetails;接口即可定义一个自定义的客户端详情对象
@Data
@AllArgsConstructor
public class CustomClientDetails implements ClientDetails {
private LoadThirdPartyPlatformsDto clientInfo;
@Override
public String getClientId() {
return this.clientInfo.getClientId().toString();
}
@Override
public Set<String> getResourceIds() {
return null;
}
@Override
public boolean isSecretRequired() {
return true;
}
@Override
public String getClientSecret() {
return this.clientInfo.getSecret();
}
@Override
public boolean isScoped() {
return true;
}
// 返回允许的授权空间
@Override
public Set<String> getScope() {
final List<AdopScopeDto> scopes = this.clientInfo.getScopes();
return scopes.stream().map(AdopScopeDto::getScopeCode).collect(Collectors.toSet());
}
// 返回允许的授权类型
@Override
public Set<String> getAuthorizedGrantTypes() {
final TreeSet<String> set = new TreeSet<>();
set.add("authorization_code");
set.add("refresh_token");
return set;
}
// 回调地址
@Override
public Set<String> getRegisteredRedirectUri() {
final String redirectUri = this.clientInfo.getRedirectUri();
return new TreeSet<String>() {{
add(redirectUri);
}};
}
@Override
public Collection<GrantedAuthority> getAuthorities() {
return new ArrayList<>();
}
@Override
public Integer getAccessTokenValiditySeconds() {
return null;
}
@Override
public Integer getRefreshTokenValiditySeconds() {
return null;
}
// 是否自动授权
@Override
public boolean isAutoApprove(String scope) {
final Boolean enableAutoConfirm = this.clientInfo.getEnableAutoConfirm();
return enableAutoConfirm != null && enableAutoConfirm;
}
@Override
public Map<String, Object> getAdditionalInformation() {
return null;
}
}
-配置资源权限服务器
用于配置每个资源访问所需的权限
// 资源服务配置
@Configuration
@EnableResourceServer // 启用资源服务
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private AdopScopeService adopScopeService;
@Override
public void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
List<AdopScope> scopeList = adopScopeService.findAll();
for (AdopScope scope : scopeList) {
registry.antMatchers(scope.getScopeUri())
.access("#oauth2.hasAnyScope('"+scope.getScopeCode()+"')")
.and()
.requestMatchers().antMatchers(scope.getScopeUri());
}
http.authorizeRequests().anyRequest().authenticated()
.and().csrf().disable();
// 旧版硬编码样例
// registry
// // 配置带资源域限制的资源信息
// .antMatchers("/resource/private/**").access("#oauth2.hasAnyScope('private')")
// .antMatchers("/resource/userInfo/**").access("#oauth2.hasAnyScope('userInfo')")
// .antMatchers("/resource/login/**").access("#oauth2.hasAnyScope('login')")
// .antMatchers("/resource/login/openId").access("#oauth2.hasAnyScope('login')")
// .and()
// // 匹配资源,对上面的资源进行匹配地址,配置在里面的资源将受到保护,必须全部认证才能访问
// // 上面配置了这个资源的访问权限。这里依然需要配置保护
// .requestMatchers()
// .antMatchers("/resource/private/**")
// .antMatchers("/resource/userInfo/**")
// .antMatchers("/resource/login/**")
// .antMatchers("/resource/login/openId")
// .and()
// // 指定任何请求,设r任何请求都需要授权以后才能访问
// .authorizeRequests().anyRequest().authenticated()
// .and().csrf().disable(); // 资源需要关闭这个,否则第三方拿到token以后依然无法访问会被拦截
}
}
-配置资源所有者加认证流程
最终目的就是验证后把UserDetails设置到SecurityContextHolder中
资Security全局配置
主要用于配置全局的访问控制,以及资源所有者的加载&登录方法
这里我们用到的流程是:主动配置加载和解密的Bean,最后通过默认的表单提交行为,或者主动触发 authenticationManager.authenticate()调用加载和校验最终实现的方法来进行用户详情对象的创建
想要定制的话可以自己添加过滤器,在喜欢的地方自己创建用户详情对象写入到SecurityContextHolder中完成身份的认证;
@Configuration
@EnableWebSecurity // 启动WebSecurity[可以写在配置类]
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomJwtTokenFilter customJwtTokenFilter;
private final UserDetailLoader userLoad;
public SecurityConfig(CustomJwtTokenFilter customJwtTokenFilter, UserDetailLoader userLoad) {
this.customJwtTokenFilter = customJwtTokenFilter;
this.userLoad = userLoad;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 单页面应用或者app可以选择关闭这个,只要不是基于会话的都可以
.cors().and() // 允许跨域
.authorizeRequests()// 配置认证请求
.antMatchers("/auth/login","/index.html") // 目前只开放鉴权入口;
.permitAll() // 对上面描述的匹配规则进行放行
// 切换到任何请求,设置都要进行认证之后才能访问
.anyRequest().authenticated();
//配置这个会造成user后的404响应,可能是因为配合了规则却没有配置后文
//http.and().requestMatchers().antMatchers("/user/**");
//http.exceptionHandling().authenticationEntryPoint(new Http403ForbiddenEntryPoint());
http.formLogin().permitAll(); // 对表单认证进行放行,同时自定义登录验证路由
// 添加jwt过滤器到密码校验之前,在那之前完成jwt的校验和放入安全上下文对象
http.addFilterBefore(customJwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* 配置用户加载器和密码校验器
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置用户加载类,以及加密方案
auth.userDetailsService(userLoad) // 用户加载类
// 这里不使用默认。使用一个自定义的方法
.passwordEncoder(new CustomJwtTokenEncoder());
}
// 当出现无法注入bean【AuthenticationManager】时添加,这个Bean用于主动调用框架的密码校验
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// 配置加密方式
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 跨域配置
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("*"); // 允许所有域访问
configuration.addAllowedMethod("*"); // 允许所有方法
configuration.addAllowedHeader("*"); // 允许所有头部
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
用户身份的解析
—方案一:登录后基于默认的会话实现
默认基于会话完成,我们可以在登录以后把这个对象设置好,在会话结束之前都可以保持登录
// 使用用户名密码创建一个用户密码对象,交给校验器校验
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getId(), "success");
// 我们预先配置好的用户加载器和密码校验器这时候就会被调用
// 手动调用security的校验方法,会调用校验管理员,触发我们定义好的用户加载和加解密校验,传入经过处理的authenticationToken
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 验证成功后得到安全上下文对象,设置到持有者中就可以了
// 将获得到的[用户安全上下文]对象设置到[安全上下文持有者]中
SecurityContextHolder.getContext().setAuthentication(authenticate);
—方案二:基于jwt实现
登录后前端保存token,后端在每一次请求来的时候解析token,并把解析的内容(id,auth,role)创建成UserDetail设置到SecurityContextHolder中
public class JwtFilter implements Filter {
private final AdminJwtUtils jwtUtils;
public JwtFilter(AdminJwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("Admin-Token");
if (StringUtils.isNotBlank(token)){
// 校验token
UserDetails user = jwtUtils.parseToken(token);
// 不为空即为校验通过
if (user!=null){
// 手动创建安全上下文,设置到线程域中
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
user,"", user.getAuthorities()
));
}
}
chain.doFilter(request, response);
}
}
客户端操作流程
-基于会话
- 使用用户密码登录后维持session
- 调用获取授权码的接口
- 收到授权码
- 调用兑换accessToken接口,传递客户端id和密码,以及授权码,获取accessToken
- 使用accessToken获取用户受保护数据
-基于Jwt
- 使用用户密码登录后把Token保存在localstorege
- 调用获取授权码的接口,同时传递Token在请求头中,
- 后台通过解析jwt维持用户登录状态,认证状态
- 收到授权码
- 调用兑换accessToken接口,传递客户端id和密码,以及授权码,获取accessToken
- 使用accessToken获取用户受保护数据
要点总结
-操作的发起主体分别是谁
- 在获取授权码的时候[/oauth/authorize],操作的主体是资源所有者,也就是拥有这个资源的用户
- 在使用授权码兑换access_token的时候[/oauth/token],操作的主体是客户端本体,需要使用客户端在平台注册的身份获取token
- 在使用access_token访问受保护资源的时候,操作的主体又是资源所有者了,也就是拥有这个资源的用户,因为这时候是客户端使用用户的临时受限身份进行资源的访问
-客户的状态如何保持
- 如果不进行处理的话,默认是基于会话进行状态保持
- 这里我使用jwt进行会话状态保持,我会解析jwt里的用户基本信息,然后创建一个安全身份上下文,传入到上下文对象中,这样鉴权过滤器就会识别到这个身份,进行放行,同时校验过滤器也会跳过
-客户端的状态如何保持
客户端是获取access_token的时候,通过表单传递客户端的client_id和client_secret进行身份状态的保持的,
用户同意授权后,并成功兑换accessToken,再次申请相同权限会自动允许
–默认方案
默认使用的是basic auth的方式进行身份认证的
basic auth的认证规范是在请求头中设置Authorization值,
Value内容格式为Basic ${Base64.create(username:password)}
以下是JS代码示例
const username = 'your_username';
const password = 'your_password';
// 将用户名和密码以 "username:password" 的形式拼接,并进行 Base64 编码
const base64Credentials = btoa(`${username}:${password}`);
// 设置请求头,包含 Authorization 字段
const headers = new Headers({
'Authorization': `Basic ${base64Credentials}`,
'Content-Type': 'application/json', // 根据你的请求需要设置其他头部
});
// 构建请求对象
const requestOptions = {
method: 'GET', // 根据你的请求类型设置
headers: headers,
// 其他请求选项(例如:body)
};
// 发起请求
fetch('https://api.example.com/resource', requestOptions)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
–自定义方案
在授权配置中重写configure(AuthorizationServerSecurityConfigurer security)添加我们自己定义的过滤器进行身份校验就可以了,校验通过以后同样创建一个UserDetail安全上下文到上下文持有者中就可以了,离谱,没想到和用户居然共用一个类
你可以把客户端的用户名和密码写在请求头里或者body里,然后取出来进行校验
/**
* 授权服务器配置
*
* @author Guochao
*/
@Configuration
@EnableAuthorizationServer // 启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* 配置自定义的客户端认证过滤器
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.addTokenEndpointAuthenticationFilter(customJwtTokenFilter);
}
}
自定义授权页面
https://baijiahao.baidu.com/s?id=1736936966974655693&wfr=spider&for=pc
授权表单的信息是基于Session保持的
也就是发起授权时保存了一个session在浏览器
然后表单提价的时候携带session进行提交,然后处理提交的表单
我们创建一个新的页面覆盖原来的/oauth/confirm_access就可以了
注意@SessionAttributes(“authorizationRequest”)一定不能少,表单是基于session维持会话的
后端部分
这里我们用到了模板引擎
@RestController
@RequestMapping(value = "/oauth")
@SessionAttributes("authorizationRequest")
public class OauthController {
private final AdopApplicationService adopApplicationService;
private final AdopScopeService adopScopeService;
public OauthController(AdopApplicationService adopApplicationService, AdopScopeService adopScopeService) {
this.adopApplicationService = adopApplicationService;
this.adopScopeService = adopScopeService;
}
@RequestMapping(value = "/confirm_access")
public ModelAndView userConfirm(Model model) {
// 这里先提取一下我们传递过来的参数,例如客户端id,state,回调地址等
final AuthorizationRequest value = (AuthorizationRequest) model.getAttribute("authorizationRequest");
if (value == null) {
throw new RuntimeException("无法获取授权请求参数");
}
final String clientId = value.getClientId();
if (StringUtils.isBlank(clientId)) {
throw new RuntimeException("没有提供客户端参数");
}
// 查询一下客户端名称方便页面显示授权方
final LoadThirdPartyPlatformsDto clientDto = Optional.ofNullable(adopApplicationService.getClientById(Long.valueOf(clientId)))
.orElseThrow(() -> new RuntimeException("客户端不存在"));
final Set<String> scope = value.getScope();
// set转换为list
final List<AdopScope> scopes = adopScopeService.findByCodes(new ArrayList<>(scope));
model.addAttribute("client", clientDto.getAppName());
model.addAttribute("scopeList", scopes);
return new ModelAndView("userConfirm.html");
}
}
前端部分
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="container">
<h1>授权认证</h1>
<p th:text="'是否授权给'+${client}+'使用您的如下资源:'"></p>
<form id="confirmationForm" name="confirmationForm" action="/open-api/oauth/authorize" method="post"><input
name="user_oauth_approval" value="true" type="hidden">
<div id="atr" th:attr="scopeList = ${scopeList}"></div>
<ul class="scope-list">
<li class="scope-item" th:each="scopeItem : ${scopeList}">
<div class="form-group">
<span th:text="${scopeItem.scopeName}"></span>
<!-- 这里的name一定要是'scope.'+scope在资源服务注册的name-->
<span class="boxes">
<input type="radio" th:name="'scope.'+${scopeItem.scopeCode}" value="true" checked="">允许
<input type="radio" th:name="'scope.'+${scopeItem.scopeCode}" value="false">拒绝
</span>
</div>
</li>
</ul>
<label class="btn-container" ><input class="submit" name="authorize" value="授权" type="submit"></label>
</form>
</div>
</body>
</html>
<style>
body,html{
padding: 0;
margin: 0;
border: none;
}
body{
display: flex;
background-color: #efeefc;
justify-content: center;
align-items: center;
height: 100vh;
color: white;
}
.container{
padding: 20px;
min-width: 400px;
border-radius: 10px;
background-color: #7e75ff;
box-shadow: 5px 5px 5px rgba(0,0,0,.1);
}
h1{
text-align: center;
}
#confirmationForm{
border: 1px;
position: relative;
}
.scope-list{
font-size: 18px;
}
.scope-item{
margin: 15px 0;
font-size: 16px;
}
.boxes{
flex-direction: row;
display: flex;
align-items: center;
font-size: 16px;
}
.btn-container{
display: block;
min-width: 100%;
text-align: center;
}
.submit{
bottom: 0px;
left: calc(50% - 100px);
border: none;
width: 200px;
height: 35px;
background-color: rgba(255,255,255,.8);
border-radius: 6px;
box-shadow: 5px 5px 5px rgba(0,0,0,.1);
}
</style>
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!