授权服务器与资源服务器分离
密码模式个人理解
认证(授权)服务器(Authorization Server):用于认证用户,如果客户端认证通过,则发放给其访问资源服务器的令牌
资源服务器(Resource Server):拥有受保护资源,如果请求包含正确的访问令牌,则可以访问资源,可以是提供管理后台、客户端 API 的服务
客户端(Client):可以是浏览器、客户端,也可以是内部服务
资源拥有者(Resource Owner):最终用户
,他有访问资源的账号与密码,可以简单把资源拥有者理解成人,他在使用客户端访问资源
(A)用户打开客户端以后,客户端要求用户给予授权
(B)用户同意给予客户端授权
(C)客户端使用上一步获得的授权,向认证服务器申请令牌
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌
(E)客户端使用令牌,向资源服务器申请获取资源
(F)资源服务器确认令牌无误,返回资源信息
上述步骤中,(B)是关键,即用户怎样才能给于客户端授权,oauth2有四种授权方式
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
密码模式
用户向客户端提供自己的用户名和密码,客户端使用这些信息,向"服务商提供商"索要授权
(AB)用户向客户端提供用户名和密码(username、password
)
(C)客户端(携带client_id、client_secret
)将用户名和密码发给认证服务器,向后者请求令牌(access_token
)
(D)认证服务器确认无误后,向客户端提供访问令牌
(E)客户端使用令牌,向资源服务器申请获取资源
(F)资源服务器确认令牌无误(与access_token包含的信息resource_id相匹配
),返回资源信息
密码模式开发过程中的参数理解
access_token:令牌,包含有该客户端拥有的resource_ids和用户信息
client_id、client_secret:证明“该客户端是可信的
”,确定你是什么客户端,可以访问什么资源服务器(可对应多个resource_id)等,没有无法获取token
username、password:具体的用户,确定你是谁,没有token无法携带用户信息
resource_id:资源服务器的标识
获取token: /oauth/token
- username:用户名
- password:用户密码
- grant_type:授权方式(password)
- client_id:客户端id
- client_secret:客户端密码
项目实践
授权服务器与资源服务器在同一个应用中的实现可见:【springboot】OAuth2+SpringSecurity+Mysql——密码模式
概述
项目包含nacos、gateway、sentinel、mybatisplus等,可按自己的需要去除无关的依赖和配置。其中nacos持久化至数据库(详细)、sentinel持久化至nacos(详细)
- **授权服务器应用(nacos-authserver)**功能:认证授权(
Authorization Server
)、用户管理(相当于Resource Server
)。
其中资源服务器
Resource Server
的用户管理功能也可以单独分离出去至另一个单独的应用。
- **资源服务器应用(nacos-producer)**功能:提供资源接口(相当于另一个
Resource Server
)、自定义登录和刷新接口(相当于客户端client
)
其中客户端
client
的功能也可以分离出去直接在前端项目实现。
- 用户角色:ADMIN、ROLE(
t_role
表) - 资源服务器id:auth_server、resource_test(
oauth_client_details
表)
mysql建表详见https://blog.csdn.net/lorogy/article/details/113695194
授权服务器项目nacos-authserver
pom.xml
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>authserver</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>authserver</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
<spring-cloud-alibaba.version>2.1.4.RELEASE</spring-cloud-alibaba.version>
<druid.version>1.1.21</druid.version>
<mybatis-plus.version>3.2.0</mybatis-plus.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos注册-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>0.9.0.RELEASE</version>
</dependency>
<!--actuator监控、sentinel限流、sentinel规则持久化至nacos-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!-- mybatisPlus -->
<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>
<!-- alibaba的druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<!-- security-oauth2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
<!--swagger相关依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</exclusion>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
<version>2.9.2</version>
</dependency>
<!--swagger防止报错Illegal DefaultValue for parameter type integer-->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.21</version>
</dependency>
<!--增强swagger-bootstrap-ui-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<!-- <scope>runtime</scope> -->
<optional>true</optional>
</dependency>
</dependencies>
<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>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.0.RELEASE</version>
</plugin>
</plugins>
</build>
</project>
application.yaml
server:
port: 18891
spring:
application:
name: nacos-authserver
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
# 配置Sentinel DashBoard地址
dashboard: 127.0.0.1:8858
# 应用与Sentinel控制台交互的端口,应用本地会起一个该端口占用的HttpServer
# 默认8719端口,假如端口被占用,依次+1,直到找到未被占用端口
port: 8091
datasource:
nacos-producer:
nacos:
serverAddr: 127.0.0.1:8848
dataId: ${spring.application.name}-sentinel-rules
groupId: SENTINEL_GROUP
dataType: json
ruleType: flow
datasource:
url: jdbc:mysql://127.0.0.1:3306/oauth?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=CTT
username: develop
password: Dev@2021
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
devtools:
restart:
enabled: true
# 需要热更新的包
additional-paths: src/main/java,src/main/resources/static
# 不需要热更新的包
exclude: src/main/java/**/CodeGenerator.java
management:
endpoints:
web:
exposure:
include: "*"
mybatis-plus:
# xml文件,classpath在这儿指/resources
mapper-locations: classpath*:com/example/authserver/mapper/xml/*.xml
global-config:
db-config:
# 主键类型
id-type: id_worker
# 字段策略
field-strategy: not_empty
# 数据库类型
db-type: mysql
configuration:
# 配置返回数据库(column下划线命名&&返回java实体是驼峰命名)
map-underscore-to-camel-case: true
# call-setters-on-nulls: true
# 打印SQL
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
cache-enabled: false
logging:
level:
root: info
Application
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(value = "com.example.authserver.mapper")
public class AuthserverApplication {
public static void main(String[] args) {
SpringApplication.run(AuthserverApplication.class, args);
}
}
重写用户认证过程
userService尽量和UserDetailServiceImpl 在同一目录
@Component
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
//重写认证的过程,由AuthenticationManager去调,从数据库中查找用户信息
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户是否存在
com.example.authserver.model.User user = userService.findByName(username);
if (user == null) {
throw new RuntimeException("username: " + username + " can not be found");
}
// 设置用户权限,可在数据库建表,通过用户查询到相应权限(角色),按以下方式加入
List<GrantedAuthority> authorities = new ArrayList<>();
ArrayList<String> list = userService.getRolesByUserId(user.getId());
if (list == null) {
throw new RuntimeException("username: " + username + " can not be found");
}
for (int i = 0; i < list.size(); i++) {
authorities.add(new SimpleGrantedAuthority(list.get(i)));
}
return new User(user.getUsername(), user.getPassword(), authorities);
}
}
授权服务配置
使用的是jwt token模式,可替换为别的
@Configuration
@EnableAuthorizationServer //注解开启了验证服务器
public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
public UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
@Autowired
private AuthenticationManager authenticationManager;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("yjy");
return converter;
}
/**
* @Description: 配置 token 节点的安全策略
* @Param: [security]
* @Return: void
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");//默认"denyAll()",不允许访问/oauth/check_token;"isAuthenticated()"需要携带auth;"permitAll()"直接访问
}
/**
* @Description: 配置客户端信息, 相当于在认证服务器中注册哪些客户端(包括资源服务器)能访问
* @Param: [clients]
* @Return: void
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource); // 设置客户端的配置从数据库中读取,存储在oauth_client_details表
}
/**
* @Description: 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
* @Param: [endpoints]
* @Return: void
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager) // 开启密码验证
.accessTokenConverter(accessTokenConverter()) // token生成方式
.userDetailsService(userDetailsService);
}
}
认证服务配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义UserDetailsService用来从数据库中根据用户名查询用户信息以及角色信息
*/
@Autowired
public UserDetailsService userDetailsService;
// 防止服务器@Autowired authenticationManager无法注入
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
/**
* @Description: 密码编码验证器
* @Param: []
* @Return: org.springframework.security.crypto.password.PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* @Description: 验证配置,第一步认证,请求需走的过滤器链
* @Param: [http]
* @Return: void
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.userDetailsService(userDetailsService);
}
}
授权服务器的功能到此已完成
接下来是资源服务器的用户管理功能:
资源服务配置
访问该资源需要客户端有auth_server
的访问权限
@Configuration
@EnableResourceServer
//@EnableGlobalMethodSecurity(prePostEnabled = true)// 单独资源服务器时或许需要
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final String RESOURCE_ID = "auth_server";
// 单独资源服务器时才需要
/*@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 单独资源服务器时才需要试
/*DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
resources.tokenServices(defaultTokenServices);*/
resources.resourceId(RESOURCE_ID).stateless(false);
}
// 单独资源服务器时才需要
/*@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("yjy");
return converter;
}*/
// 单独资源服务器时才需要
/*@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}*/
/**
* @Description: 设置资源访问权限需要重写该方法
* @Param: [http]
* @Return: void
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/**").authenticated();
}
}
用户管理功能
@PreAuthorize("ADMIN")
表示用户需要有权限(角色)ADMIN
,上文重写的认证过程loadUserByUsername
中将用户的角色role
加入了authorities
。
@RestController
@RequestMapping("/user")
@Api(value = "user", tags = "用户管理")
public class UserController {
@Autowired
private UserService userService;
/**
* 根据id获取用户
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation(value="查询用户",notes="需要ADMIN角色用户")
@PreAuthorize("ADMIN")
public JsonResult getUser(@PathVariable("id") Long id){
return JsonResult.ok(userService.getUserById(id));
}
/**
* 获取登录用户相关信息
* @return
*/
@GetMapping("/userinfo")
@ApiOperation(value="获取登录用户相关信息")
public JsonResult getUserInfo(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Long id= userService.findByName(authentication.getName()).getId();
return JsonResult.ok(userService.getUserInfoById(id));
}
/**
* 分页获取所有用户列表
* @param pageNum
* @param pageSize
* @return
*/
@GetMapping("/list")
@PreAuthorize("ADMIN")
@ApiOperation(value="查询所有用户",notes="需要ADMIN角色用户")
public JsonResult list(@RequestParam Integer pageNum, @RequestParam Integer pageSize){
PageEntity pageEntity=new PageEntity();
pageEntity.setPageNum(pageNum);
pageEntity.setPageSize(pageSize);
return JsonResult.ok(userService.selectAllUser(pageEntity));
}
/**
* 增加用户
* @param user
* @return
*/
@PostMapping
@ApiOperation(value="增加用户",notes="用户名和密码必填")
public JsonResult addUser(@RequestBody User user){
return JsonResult.ok(userService.addUser(user));
}
/**
* 修改用户
* @param user
* @return
*/
@PutMapping
@ApiOperation(value="修改用户")
public JsonResult editUser(@RequestBody User user){
return JsonResult.ok(userService.updateUser(user));
}
/**
* 删除用户
* @param id
* @return
*/
@DeleteMapping("/{id}")
@PreAuthorize("ADMIN")
@ApiOperation(value="删除用户",notes="需要ADMIN角色用户")
public JsonResult delUser(@PathVariable("id") Long id){
return JsonResult.ok(userService.deleteUser(id));
}
}
资源服务器项目nacos-producer
pom.xml
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>producer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>producer</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
<spring-cloud-alibaba.version>2.1.4.RELEASE</spring-cloud-alibaba.version>
<druid.version>1.1.21</druid.version>
<mybatis-plus.version>3.2.0</mybatis-plus.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos注册-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>0.9.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--actuator监控、sentinel限流、sentinel规则持久化至nacos-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!-- mybatisPlus -->
<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>
<!-- alibaba的druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<!-- security-oauth2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
<!--swagger相关依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</exclusion>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
<version>2.9.2</version>
</dependency>
<!--swagger防止报错Illegal DefaultValue for parameter type integer-->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.21</version>
</dependency>
<!--增强swagger-bootstrap-ui-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<!-- <scope>runtime</scope> -->
<optional>true</optional>
</dependency>
</dependencies>
<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>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.0.RELEASE</version>
</plugin>
</plugins>
</build>
</project>
application.yaml
端口18890
的uri是gateway
网关
server:
port: 18892
spring:
application:
name: nacos-producer
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
# 配置Sentinel DashBoard地址
dashboard: 127.0.0.1:8858
# 应用与Sentinel控制台交互的端口,应用本地会起一个该端口占用的HttpServer
# 默认8719端口,假如端口被占用,依次+1,直到找到未被占用端口
port: 8091
datasource:
nacos-producer:
nacos:
serverAddr: 127.0.0.1:8848
dataId: ${spring.application.name}-sentinel-rules
groupId: SENTINEL_GROUP
dataType: json
ruleType: flow
datasource:
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=CTT
username: develop
password: Dev@2021
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
devtools:
restart:
enabled: true
# 需要热更新的包
additional-paths: src/main/java,src/main/resources/static
# 不需要热更新的包
exclude: src/main/java/**/CodeGenerator.java
management:
endpoints:
web:
exposure:
include: "*"
mybatis-plus:
# xml文件,classpath在这儿指/resources
mapper-locations: classpath*:com/example/producer/mapper/xml/*.xml
global-config:
db-config:
# 主键类型
id-type: id_worker
# 字段策略
field-strategy: not_empty
# 数据库类型
db-type: mysql
configuration:
# 配置返回数据库(column下划线命名&&返回java实体是驼峰命名)
map-underscore-to-camel-case: true
# call-setters-on-nulls: true
# 打印SQL
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
cache-enabled: false
security:
oauth2:
# 对应 OAuth2ClientProperties 类
#客户端认证,这里配置相当于该资源服务器同时也是一个客户端,可以这里不配置在前端配置
client:
client-id: test_client
client-secret: user
#对应 ResourceServerProperties 类
#校验访问令牌的有效性
resource:
user-info-uri: http:/127.0.0.1:18890/nacos-authserver/oauth/check_token
#自定义,获取访问令牌,用于实现/login接口
access-token-uri: http://127.0.0.1:18890/nacos-authserver/oauth/token
logging:
level:
root: info
Application
@EnableFeignClients
是为了远程调用nacos-authserver
用户管理的接口
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan(value = "com.example.producer.mapper")
public class ProducerApplication {
public static void main(String[] args) {
SpringApplication.run(ProducerApplication.class, args);
}
}
资源服务配置
访问该资源需要客户端有resource_test
的访问权限
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final String RESOURCE_ID = "resource_test";
// 单独资源服务器时或许需要
@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 单独资源服务器时或许需要
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
resources.tokenServices(defaultTokenServices);
resources.resourceId(RESOURCE_ID).stateless(false);
}
// 单独资源服务器时或许需要
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("yjy");
return converter;
}
// 单独资源服务器时或许需要
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
/**
* @Description: 设置资源访问权限需要重写该方法
* @Param: [http]
* @Return: void
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/refresh").permitAll()
.antMatchers("/user/userinfo").permitAll()
.antMatchers("/test/**").authenticated();
}
}
测试接口
@PreAuthorize("hasRole('ROLE_ADMIN')")
@RestController
@RequestMapping(value = "/test")
@Api(value = "test", tags = "测试接口")
public class HelloController {
@GetMapping("hello")
public String hello(@RequestParam(defaultValue = "producer",required = false) String name){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return "hello "+authentication.getName()+","+name+" is ready";
}
@GetMapping("test1")
public String test1(){
return "producer test1";
}
@GetMapping("test2")
public String test2(){
return "producer test2";
}
@GetMapping("/project")
@PreAuthorize("hasRole('ROLE_ADMIN')") //具有此角色才可以访问
public String project(){
return "This is my project";
}
}
资源服务器已经完成
使用从授权服务器获取的令牌access_token
即可访问该资源
接下来是将该资源服务器同时作为客户端,自定义/login
和/refresh
接口,以及远程调用用户信息接口,目的是前端应用用户只需要账密登录,不存储client_id
等相关客户端信息
自定义/login
和/refresh
接口
/**
* 自定义登录接口和更新接口
* 此处相当于一个oauth的客户端,调用授权服务器 /oauth/token,使用密码模式获取访问令牌
*/
@RestController
@RequestMapping("/")
public class LoginController {
@Autowired
private OAuth2ClientProperties oAuth2ClientProperties;
@Value("${security.oauth2.access-token-uri}")
private String accessTokenUri;
@PostMapping("login")
public OAuth2AccessToken login(@RequestParam("username") String username,
@RequestParam("password") String password){
// 创建 ResourceOwnerPasswordResourceDetails 对象,填写密码模式授权需要的请求参数
ResourceOwnerPasswordResourceDetails resourceDetails=new ResourceOwnerPasswordResourceDetails();
resourceDetails.setAccessTokenUri(accessTokenUri);
resourceDetails.setClientId(oAuth2ClientProperties.getClientId());
resourceDetails.setClientSecret(oAuth2ClientProperties.getClientSecret());
resourceDetails.setUsername(username);
resourceDetails.setPassword(password);
// 创建 OAuth2RestTemplate 对象,是 Spring Security OAuth 封装的工具类,用于请求授权服务器
OAuth2RestTemplate restTemplate=new OAuth2RestTemplate(resourceDetails);
// 将 ResourceOwnerPasswordAccessTokenProvider 设置到其中,表示使用密码模式授权
restTemplate.setAccessTokenProvider(new ResourceOwnerPasswordAccessTokenProvider());
// 获取访问令牌
return restTemplate.getAccessToken();
}
@PostMapping("refresh")
public Object refresh(@RequestParam("refresh_token") String refreshToken){
RestTemplate restTemplate=new RestTemplate();
HttpHeaders headers=new HttpHeaders();
headers.setBasicAuth(oAuth2ClientProperties.getClientId(),oAuth2ClientProperties.getClientSecret());
String url=accessTokenUri+"?grant_type=refresh_token&refresh_token="+refreshToken;
HttpEntity<HashMap<String, Object>> request = new HttpEntity<>(headers);
JSONObject jsonObject=restTemplate.postForObject(url,request,JSONObject.class);
return jsonObject;
}
}
Feign远程调用
也可通过Feign
远程调用nacos-authserver
服务的接口,直接调用会因未携带令牌信息报错401,具体调用方法可见https://blog.csdn.net/lorogy/article/details/117712001