接上篇文章,在这个流程中,PostMan可以代表客户端应用,订单服务是资源服务器,唯一缺少的是 认证服务器 ,下面来搭建认证服务器
项目结构:
Pom.xml : DependencyManager 引入SpringCloud的配置,Dependency引入 spring-cloud-starter-oauth2
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.nb.security</groupId> <artifactId>nb-server-auth</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <mybatis-plus.version>3.1.2</mybatis-plus.version> <java.version>1.8</java.version> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <mybatis-plus.version>3.1.2</mybatis-plus.version> <druid.version>1.1.17</druid.version> <jwt.version>0.9.1</jwt.version> <commons.version>2.6</commons.version> <aliyun-java-sdk-core.version>3.2.3</aliyun-java-sdk-core.version> <aliyun-java-sdk-dysmsapi.version>1.0.0</aliyun-java-sdk-dysmsapi.version> <aliyun.oss.version>3.6.0</aliyun.oss.version> <qc.cos.version>5.6.5</qc.cos.version> </properties> <dependencyManagement> <dependencies> <dependency> <!-- Import dependency management from Spring Boot --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.1.6.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!--spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.SR2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <!--集成mybatisplus--> <!-- mybatis-plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.1</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.29</version> </dependency> <!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> <scope>runtime</scope> </dependency> <!--commons-lang3--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> </dependencies> <build> <plugins> <!--指定JDK编译版本 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <!-- 打包跳过测试 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <skipTests>true</skipTests> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.yml :
server: port: 9090 spring: application: name: auth-server datasource: url: jdbc:mysql://localhost:3306/db_oauth?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8 username: root password: root driver-class-name: com.mysql.jdbc.Driver #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:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)"; id-type: 0 # 默认数据库表下划线命名 table-underline: true configuration: # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
下面开始新建认证服务器的配置类,写代码之前,先来看一下下图,
作为认证服务器,OAuth2 协议里的其他几个角色他都要知道,认证服务器都要知道各个角色都是谁,他们各自的特征是什么。
1,要知道有哪些客户端应用 来申请令牌
2,要知道有哪些合法的用户
3,要知道发出去的令牌,能够访问哪些资源服务器
写代码
我们需要新建一个认证服务器配置类 OAuth2AuthServerConfig ,继承 AuthorizationServerConfigurerAdapter ,AuthorizationServerConfigurerAdapter 是认证服务器适配器,我们看一下的源码:
/** * @author Dave Syer * */ public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer { @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { } }
里面有三个方法,这三个方法,正对应上图中箭头所指的三个问题,我们需要重写这三个方法,实现自己的配置。
1,配置Client信息
从图中可以看出,认证服务器要配置两个Client,一个是【客户端应用】,他需要来认证服务器申请令牌,一个是 【订单服务】,他要来认证服务器验令牌。
重写AuthorizationServerConfigurerAdapter 的 configure(ClientDetailsServiceConfigurer clients) throws Exception 方法
/** * Created by: 李浩洋 on 2019-10-29 * * 认证服务器 **/ @Configuration //这是一个配置类 @EnableAuthorizationServer //当前应用是一个认证服务器 public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {//AuthorizationServerConfigurerAdapter:认证服务器适配器 //Spring 对密码加密的封装,自己配置下 @Autowired private PasswordEncoder passwordEncoder; /** * 配置客户端应用的信息,让认证服务器知道有哪些客户端应用来申请令牌。 * * ClientDetailsServiceConfigurer:客户端的详情服务的配置 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory()//配置在内存里,后面修改为数据库里 //~============== 注册【客户端应用】,使客户端应用能够访问认证服务器 =========== .withClient("orderApp") .secret(passwordEncoder.encode("123456")) //spring .scopes("read","write") //orderApp有哪些权限 .accessTokenValiditySeconds(3600) //token的有效期 .resourceIds("order-server") //资源服务器的id。发给orderApp的token,能访问哪些资源服务器,可以多个 .authorizedGrantTypes("password")//授权方式,再给orderApp做授权的时候可以用哪种授权方式授权 //~=============客户端应用配置结束 ===================== .and() //~============== 注册【资源服务器-订单服务】(因为订单服务需要来认证服务器验令牌),使订单服务也能够访问认证服务器 =========== .withClient("orderServer") .secret(passwordEncoder.encode("123456")) //spring .scopes("read","write") //有哪些权限 .accessTokenValiditySeconds(3600) //token的有效期 .resourceIds("order-server") //资源服务器的id .authorizedGrantTypes("password");//授权方式 } }
2,配置用户 信息
告诉认证服务器,有哪些用户可以来访问认证服务器
重写AuthorizationServerConfigurerAdapter 的 configure(AuthorizationServerEndpointsConfigurer endpoints) 方法
/** * Created by: 李浩洋 on 2019-10-29 * * 认证服务器 **/ @Configuration //这是一个配置类 @EnableAuthorizationServer //当前应用是一个认证服务器 public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {//AuthorizationServerConfigurerAdapter:认证服务器适配器 //Spring 对密码加密的封装,自己配置下 @Autowired private PasswordEncoder passwordEncoder; @Autowired private AuthenticationManager authenticationManager; /** * 1,配置客户端应用的信息,让认证服务器知道有哪些客户端应用来申请令牌。 * * ClientDetailsServiceConfigurer:客户端的详情服务的配置 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory()//配置在内存里,后面修改为数据库里 //~============== 注册【客户端应用】,使客户端应用能够访问认证服务器 =========== .withClient("orderApp") .secret(passwordEncoder.encode("123456")) //spring .scopes("read","write") //orderApp有哪些权限 .accessTokenValiditySeconds(3600) //token的有效期 .resourceIds("order-server") //资源服务器的id。发给orderApp的token,能访问哪些资源服务器,可以多个 .authorizedGrantTypes("password")//授权方式,再给orderApp做授权的时候可以用哪种授权方式授权 //~=============客户端应用配置结束 ===================== .and() //~============== 注册【资源服务器-订单服务】(因为订单服务需要来认证服务器验令牌),使订单服务也能够访问认证服务器 =========== .withClient("orderServer") .secret(passwordEncoder.encode("123456")) //spring .scopes("read","write") //orderServer有哪些权限 .accessTokenValiditySeconds(3600) //token的有效期 .resourceIds("order-server") //资源服务器的id。 .authorizedGrantTypes("password");//授权方式, } /** *,2,配置用户信息 * @param endpoints * @throws Exception */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //传给他一个authenticationManager用来校验传过来的用户信息是不是合法的,注进来一个,自己实现 endpoints.authenticationManager(authenticationManager); } /** * 3,配置资源服务器过来验token 的规则 * @param security * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { /** * 过来验令牌有效性的请求,不是谁都能验的,必须要是经过身份认证的。 * 所谓身份认证就是,必须携带clientId,clientSecret,否则随便一请求过来验token是不验的 */ security.checkTokenAccess("isAuthenticated()"); } }
上边的 加密解密类 PasswordEncoder 和 配置用户信息的 AuthenticationManager 还没有地方来,下边配置这俩类。
新建配置类 OAuth2WebSecurityConfig 继承 WebSecurityConfigurerAdapter
package com.nb.security.server.auth; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * Created by: 李浩洋 on 2019-10-29 **/ @Configuration @EnableWebSecurity //使安全配置生效 public class OAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; /** * AuthenticationManagerBuilder 是用来构建 AuthenticationManager(处理登录操作)的 * 需要两个东西:userDetailsService 、passwordEncoder * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) //获取用户信息 .passwordEncoder(passwordEncoder); //比对密码 } /** * 把AuthenticationManager暴露为bean * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
+++++++++++++++ 备注 ++++++++++++++++++++++++++
2019-12-15-23:00, passwordEncoder 本应该配置在上述类里,但是配置后报错循环依赖,暂时将其写在启动类里,不报错
+++++++++++++++++++++++++++++++++++++++++++++
UserDetailsService接口
UserDetailsService接口,只有一个方法,返回UserDetails 接口:
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
loadUserByUsername,这里不用比对密码,比对密码是在AuthenticationManager里做的。
UserDetails接口如下,提供了一些见名知意的方法,我们需要自定义自己的UserDetails实现类,比如你的User类实现这个接口 :
public interface UserDetails extends Serializable { // ~ Methods // ======================================================================================================== Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
自定义UserDetailsService 实现类:
/** * Created by: 李浩洋 on 2019-10-29 **/ @Component//TODO:这里不写 ("userDetailsService") public class UserDetailsServiceImpl implements UserDetailsService { /** * */ @Autowired private PasswordEncoder passwordEncoder; public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return User.withUsername(username) .password(passwordEncoder.encode("123456")) .authorities("ROLE_ADMIN") //权限 .build();//构建一个User对象 } }
AuthenticationManager 接口
也只有一个方法,实现类一般不需要自己实现。入参 是一个 Authentication 接口的实现,其中封装了认证的信息,不同的认证发的信息不一样,如用户名/密码 登录需要用户名密码,OAuth2则需要appId,appSecret,redirectURI等,
不同的认证方式传过来的实现不同, authenticate 方法验证完了后将其中的信息更新调,返回。
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
所有的准备工作都做好了,下面启动应用,来申请一个OAuth2令牌
postman请求http://localhost:9090/oauth/token ,HttpBasic传入客户端id和客户端密码。
用户名随便输,密码要和UserDetailsService一致,grant_type是说OAuth2的授权类型是授权码类型,scope是该客户端申请的权限,必须在Client配置里面包含
输入错误的密码:
输入不存在的scope:
代码github:https://github.com/lhy1234/springcloud-security/tree/chapt-4-4-andbefore
+++++++++++++分割线++++++++++++++++++++++
小结
用图片生动解释了 OAuth2 中的角色和Spring接口 AuthorizationServerConfigurerAdapter 三个方法的关系,方便记忆
实现了OAuth2的密码模式,来申请token
存在问题:passwordEncoder 如果放在了 OAuth2WebSecurityConfig配置类里面,就会报循环依赖错误,有待解决
欢迎关注个人公众号一起交流学习:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?