赞
踩
业务流程
集成流程
// 不为空再进行安全上下文的生成和赋予;如果为空直接放行,下一个过滤器会收拾他,不过不要修改加解密bean
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getId(), "success");
// 手动调用security的校验方法,会调用校验管理员,触发我们定义好的用户加载和加解密校验,传入经过处理的authenticationToken
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 将获得到的[用户安全上下文]对象设置到[安全上下文持有者]中
SecurityContextHolder.getContext().setAuthentication(authenticate);
总而言之就是要:
<?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>
这里的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"); } } } }
客户端认证时查询服务器获取结果
/** * 授权服务器配置 * * @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中
主要用于配置全局的访问控制,以及资源所有者的加载&登录方法
这里我们用到的流程是:主动配置加载和解密的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);
登录后前端保存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); } }
客户端是获取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>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。