├── demo-frontend ├── src │ ├── assets │ │ └── .gitkeep │ ├── app │ │ ├── pages │ │ │ ├── index │ │ │ │ ├── index.component.scss │ │ │ │ ├── index.component.html │ │ │ │ ├── index.component.ts │ │ │ │ └── index.component.spec.ts │ │ │ ├── show-user-list │ │ │ │ ├── show-user-list.component.scss │ │ │ │ ├── show-user-list.component.html │ │ │ │ ├── show-user-list.component.spec.ts │ │ │ │ └── show-user-list.component.ts │ │ │ ├── show-user-detail │ │ │ │ ├── show-user-detail.component.scss │ │ │ │ ├── show-user-detail.component.spec.ts │ │ │ │ ├── show-user-detail.component.ts │ │ │ │ └── show-user-detail.component.html │ │ │ ├── show-personal-role │ │ │ │ ├── show-personal-role.component.scss │ │ │ │ ├── show-personal-role.component.html │ │ │ │ ├── show-personal-role.component.ts │ │ │ │ └── show-personal-role.component.spec.ts │ │ │ ├── show-personal-detail │ │ │ │ ├── show-personal-detail.component.scss │ │ │ │ ├── show-personal-detail.component.ts │ │ │ │ ├── show-personal-detail.component.spec.ts │ │ │ │ └── show-personal-detail.component.html │ │ │ ├── update-personal-detail │ │ │ │ ├── update-personal-detail.component.scss │ │ │ │ ├── update-personal-detail.component.spec.ts │ │ │ │ ├── update-personal-detail.component.ts │ │ │ │ └── update-personal-detail.component.html │ │ │ ├── register │ │ │ │ ├── register.component.scss │ │ │ │ ├── register.component.spec.ts │ │ │ │ ├── register.component.html │ │ │ │ └── register.component.ts │ │ │ ├── login │ │ │ │ ├── login.component.scss │ │ │ │ ├── login.component.spec.ts │ │ │ │ ├── login.component.html │ │ │ │ └── login.component.ts │ │ │ └── reset-user-password │ │ │ │ ├── reset-user-password.component.scss │ │ │ │ ├── reset-user-password.component.spec.ts │ │ │ │ ├── reset-user-password.component.html │ │ │ │ └── reset-user-password.component.ts │ │ ├── shared │ │ │ ├── pipe │ │ │ │ ├── gender.pipe.spec.ts │ │ │ │ └── gender.pipe.ts │ │ │ ├── guard │ │ │ │ ├── auth.guard.spec.ts │ │ │ │ ├── login.guard.spec.ts │ │ │ │ ├── auth.guard.ts │ │ │ │ └── login.guard.ts │ │ │ ├── common │ │ │ │ ├── common.service.spec.ts │ │ │ │ ├── auth.interceptor.spec.ts │ │ │ │ ├── auth.interceptor.ts │ │ │ │ └── common.service.ts │ │ │ ├── icons-provider.module.ts │ │ │ └── shared.module.ts │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.component.scss │ │ ├── app.component.html │ │ └── app.module.ts │ ├── favicon.ico │ ├── styles.scss │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── theme.less │ ├── index.html │ ├── main.ts │ ├── test.ts │ └── polyfills.ts ├── proxy.json ├── .editorconfig ├── tsconfig.app.json ├── tsconfig.spec.json ├── .browserslistrc ├── .gitignore ├── tsconfig.json ├── README.md ├── package.json ├── karma.conf.js └── angular.json ├── docs └── img │ ├── register.png │ ├── userlist.png │ ├── 文件目录结构0.png │ ├── 文件目录结构1.png │ ├── 文件目录结构2.png │ ├── 文件目录结构3.png │ ├── userdetail.png │ └── changepassword.png ├── authorization-server ├── src │ ├── main │ │ ├── resources │ │ │ ├── application.yml │ │ │ ├── application-default.yml │ │ │ ├── mapper │ │ │ │ ├── RoleMapper.xml │ │ │ │ ├── ResourceMapper.xml │ │ │ │ ├── ClientMapper.xml │ │ │ │ └── UserMapper.xml │ │ │ └── generator │ │ │ │ └── generatorConfig.xml │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ ├── generator │ │ │ ├── mapper │ │ │ │ ├── RoleMapper.java │ │ │ │ ├── ClientMapper.java │ │ │ │ ├── ResourceMapper.java │ │ │ │ └── UserMapper.java │ │ │ └── model │ │ │ │ ├── Role.java │ │ │ │ ├── Resource.java │ │ │ │ ├── User.java │ │ │ │ └── Client.java │ │ │ ├── config │ │ │ ├── MyBatisConfig.java │ │ │ ├── ValidatorConfig.java │ │ │ └── security │ │ │ │ ├── CustomLogoutHandler.java │ │ │ │ ├── WebSecurityConfig.java │ │ │ │ └── AuthConfig.java │ │ │ ├── user │ │ │ ├── bean │ │ │ │ ├── RoleVO.java │ │ │ │ ├── UserChangePasswordVO.java │ │ │ │ ├── UserRegisterVO.java │ │ │ │ └── UserVO.java │ │ │ ├── UserController.java │ │ │ ├── valid │ │ │ │ ├── ValidUsername.java │ │ │ │ └── ValidToken.java │ │ │ └── UserService.java │ │ │ ├── AuthorizationServiceApplication.java │ │ │ └── client │ │ │ ├── ClientService.java │ │ │ └── ClientVO.java │ └── test │ │ ├── java │ │ └── com │ │ │ └── example │ │ │ ├── AuthorizationServiceApplicationTests.java │ │ │ ├── user │ │ │ ├── UserControllerTest.java │ │ │ ├── UserServiceTest.java │ │ │ └── UserMapperTest.java │ │ │ ├── utils │ │ │ └── TestRequestUtils.java │ │ │ └── auth │ │ │ └── AuthTest.java │ │ └── resources │ │ ├── application.yaml │ │ └── sql │ │ ├── unit-test-data.sql │ │ └── unit-test-schema.sql └── pom.xml ├── resource-server ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ ├── generator │ │ │ │ ├── mapper │ │ │ │ │ ├── UserDetailMapper.java │ │ │ │ │ └── UserMapper.java │ │ │ │ └── model │ │ │ │ │ ├── User.java │ │ │ │ │ └── UserDetail.java │ │ │ │ ├── user │ │ │ │ ├── vo │ │ │ │ │ ├── UserVO.java │ │ │ │ │ ├── ResetUserPasswordVO.java │ │ │ │ │ └── UserDetailVO.java │ │ │ │ ├── UserService.java │ │ │ │ └── UserController.java │ │ │ │ ├── config │ │ │ │ ├── MyBatisConfig.java │ │ │ │ ├── ResourceConfig.java │ │ │ │ ├── WebSecurityConfig.java │ │ │ │ └── ValidatorConfig.java │ │ │ │ └── ResourceServiceApplication.java │ │ └── resources │ │ │ ├── application-default.yml │ │ │ ├── application.yml │ │ │ ├── mapper │ │ │ ├── UserDetailMapper.xml │ │ │ └── UserMapper.xml │ │ │ └── generator │ │ │ └── generatorConfig.xml │ └── test │ │ ├── java │ │ └── com │ │ │ └── example │ │ │ ├── ResourceServiceApplicationTests.java │ │ │ ├── config │ │ │ └── TestResourceConfig.java │ │ │ └── user │ │ │ ├── UserMapperTest.java │ │ │ ├── UserServiceTest.java │ │ │ ├── UserProcessByMySQLModeTest.java │ │ │ ├── UserControllerTest.java │ │ │ └── UserProcessTest.java │ │ └── resources │ │ ├── sql │ │ ├── unit-test-data.sql │ │ └── unit-test-schema.sql │ │ └── application.yaml └── pom.xml ├── .gitignore ├── pom.xml ├── LICENSE ├── README.md └── sql ├── initial-data.sql └── schema.sql /demo-frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/index/index.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-user-list/show-user-list.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-user-detail/show-user-detail.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-personal-role/show-personal-role.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-personal-detail/show-personal-detail.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/update-personal-detail/update-personal-detail.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/img/register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq253498229/spring-boot-oauth2-example/HEAD/docs/img/register.png -------------------------------------------------------------------------------- /docs/img/userlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq253498229/spring-boot-oauth2-example/HEAD/docs/img/userlist.png -------------------------------------------------------------------------------- /docs/img/文件目录结构0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq253498229/spring-boot-oauth2-example/HEAD/docs/img/文件目录结构0.png -------------------------------------------------------------------------------- /docs/img/文件目录结构1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq253498229/spring-boot-oauth2-example/HEAD/docs/img/文件目录结构1.png -------------------------------------------------------------------------------- /docs/img/文件目录结构2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq253498229/spring-boot-oauth2-example/HEAD/docs/img/文件目录结构2.png -------------------------------------------------------------------------------- /docs/img/文件目录结构3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq253498229/spring-boot-oauth2-example/HEAD/docs/img/文件目录结构3.png -------------------------------------------------------------------------------- /docs/img/userdetail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq253498229/spring-boot-oauth2-example/HEAD/docs/img/userdetail.png -------------------------------------------------------------------------------- /docs/img/changepassword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq253498229/spring-boot-oauth2-example/HEAD/docs/img/changepassword.png -------------------------------------------------------------------------------- /demo-frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qq253498229/spring-boot-oauth2-example/HEAD/demo-frontend/src/favicon.ico -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-personal-role/show-personal-role.component.html: -------------------------------------------------------------------------------- 1 |

您的角色:

2 |

- {{item}}

3 | -------------------------------------------------------------------------------- /demo-frontend/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | .right { 4 | float: right; 5 | } 6 | -------------------------------------------------------------------------------- /demo-frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | oauthUrl: 'http://localhost:8080', 4 | api: 'http://localhost:8081' 5 | }; 6 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/index/index.component.html: -------------------------------------------------------------------------------- 1 |

这个是主页

2 |

这个页面的内容是公开的,谁都能看到

3 |

要想看到私有页面请先登录

4 |

登录账号:

5 |

普通用户-用户名密码都是: user

6 |

管理员用户-用户名密码都是: admin

7 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/register/register.component.scss: -------------------------------------------------------------------------------- 1 | [nz-form] { 2 | max-width: 600px; 3 | } 4 | 5 | .phone-select { 6 | width: 70px; 7 | } 8 | 9 | .register-are { 10 | margin-bottom: 8px; 11 | } 12 | -------------------------------------------------------------------------------- /authorization-server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | mybatis: 2 | mapper-locations: classpath*:mapper/*.xml 3 | spring: 4 | main: 5 | allow-circular-references: true 6 | server: 7 | error: 8 | include-binding-errors: always 9 | include-message: always -------------------------------------------------------------------------------- /demo-frontend/src/app/shared/pipe/gender.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import {GenderPipe} from './gender.pipe'; 2 | 3 | describe('GenderPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new GenderPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/generator/mapper/RoleMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.generator.mapper; 2 | 3 | import com.example.generator.model.Role; 4 | import tk.mybatis.mapper.common.Mapper; 5 | 6 | public interface RoleMapper extends Mapper { 7 | } -------------------------------------------------------------------------------- /demo-frontend/proxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "/user": { 3 | "target": "http://localhost:8081", 4 | "secure": false, 5 | "ws": true 6 | }, 7 | "/oauth": { 8 | "target": "http://localhost:8080", 9 | "secure": false, 10 | "ws": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/generator/mapper/ClientMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.generator.mapper; 2 | 3 | import com.example.generator.model.Client; 4 | import tk.mybatis.mapper.common.Mapper; 5 | 6 | public interface ClientMapper extends Mapper { 7 | } -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/generator/mapper/ResourceMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.generator.mapper; 2 | 3 | import com.example.generator.model.Resource; 4 | import tk.mybatis.mapper.common.Mapper; 5 | 6 | public interface ResourceMapper extends Mapper { 7 | } -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/login/login.component.scss: -------------------------------------------------------------------------------- 1 | .login-form { 2 | max-width: 300px; 3 | } 4 | 5 | .login-form-margin { 6 | margin-bottom: 16px; 7 | } 8 | 9 | .login-form-forgot { 10 | float: right; 11 | } 12 | 13 | .login-form-button { 14 | width: 100%; 15 | } 16 | -------------------------------------------------------------------------------- /resource-server/src/main/java/com/example/generator/mapper/UserDetailMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.generator.mapper; 2 | 3 | import com.example.generator.model.UserDetail; 4 | import tk.mybatis.mapper.common.Mapper; 5 | 6 | public interface UserDetailMapper extends Mapper { 7 | } -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/reset-user-password/reset-user-password.component.scss: -------------------------------------------------------------------------------- 1 | .login-form { 2 | max-width: 300px; 3 | } 4 | 5 | .login-form-margin { 6 | margin-bottom: 16px; 7 | } 8 | 9 | .login-form-forgot { 10 | float: right; 11 | } 12 | 13 | .login-form-button { 14 | width: 100%; 15 | } 16 | -------------------------------------------------------------------------------- /resource-server/src/main/java/com/example/user/vo/UserVO.java: -------------------------------------------------------------------------------- 1 | package com.example.user.vo; 2 | 3 | import com.example.generator.model.UserDetail; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | @EqualsAndHashCode(callSuper = true) 8 | @Data 9 | public class UserVO extends UserDetail { 10 | private String username; 11 | } 12 | -------------------------------------------------------------------------------- /resource-server/src/test/java/com/example/ResourceServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ResourceServiceApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /authorization-server/src/test/java/com/example/AuthorizationServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class AuthorizationServiceApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /demo-frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /demo-frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /resource-server/src/main/java/com/example/config/MyBatisConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import tk.mybatis.spring.annotation.MapperScan; 5 | 6 | /** 7 | * MyBatis相关配置 8 | */ 9 | @Configuration 10 | @MapperScan(basePackages = "com.example.generator.mapper") 11 | public class MyBatisConfig { 12 | } 13 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/config/MyBatisConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import tk.mybatis.spring.annotation.MapperScan; 5 | 6 | /** 7 | * MyBatis相关配置 8 | */ 9 | @Configuration 10 | @MapperScan(basePackages = "com.example.generator.mapper") 11 | public class MyBatisConfig { 12 | } 13 | -------------------------------------------------------------------------------- /demo-frontend/src/theme.less: -------------------------------------------------------------------------------- 1 | // Custom Theming for NG-ZORRO 2 | // For more information: https://ng.ant.design/docs/customize-theme/en 3 | @import "../node_modules/ng-zorro-antd/ng-zorro-antd.less"; 4 | 5 | // Override less variables to here 6 | // View all variables: https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/components/style/themes/default.less 7 | 8 | // @primary-color: #1890ff; 9 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/index/index.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-index', 5 | templateUrl: './index.component.html', 6 | styleUrls: ['./index.component.scss'] 7 | }) 8 | export class IndexComponent implements OnInit { 9 | 10 | constructor() { 11 | } 12 | 13 | ngOnInit(): void { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /resource-server/src/main/java/com/example/user/vo/ResetUserPasswordVO.java: -------------------------------------------------------------------------------- 1 | package com.example.user.vo; 2 | 3 | import lombok.Data; 4 | 5 | import javax.validation.constraints.NotBlank; 6 | 7 | @Data 8 | public class ResetUserPasswordVO { 9 | @NotBlank(message = "用户名不能为空") 10 | private String username; 11 | @NotBlank(message = "密码不能为空") 12 | private String password; 13 | } 14 | -------------------------------------------------------------------------------- /demo-frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SpringBoot Angular OAuth2 Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resource-server/src/main/resources/application-default.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | username: root 4 | password: root 5 | url: jdbc:mysql://localhost:3306/spring-boot-oauth2-example 6 | driver-class-name: com.mysql.cj.jdbc.Driver 7 | logging: 8 | pattern: 9 | console: "%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}" 10 | -------------------------------------------------------------------------------- /authorization-server/src/main/resources/application-default.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | username: root 4 | password: root 5 | url: jdbc:mysql://localhost:3306/spring-boot-oauth2-example 6 | driver-class-name: com.mysql.cj.jdbc.Driver 7 | logging: 8 | pattern: 9 | console: "%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}" 10 | -------------------------------------------------------------------------------- /authorization-server/src/test/java/com/example/user/UserControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.example.user; 2 | 3 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 4 | import org.springframework.test.web.servlet.MockMvc; 5 | 6 | import javax.annotation.Resource; 7 | 8 | /** 9 | * 用户接口测试 10 | */ 11 | @WebMvcTest(UserController.class) 12 | class UserControllerTest { 13 | @Resource 14 | MockMvc mockMvc; 15 | } -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/user/bean/RoleVO.java: -------------------------------------------------------------------------------- 1 | package com.example.user.bean; 2 | 3 | import com.example.generator.model.Resource; 4 | import com.example.generator.model.Role; 5 | import lombok.Data; 6 | import lombok.EqualsAndHashCode; 7 | 8 | import java.util.List; 9 | 10 | @EqualsAndHashCode(callSuper = true) 11 | @Data 12 | public class RoleVO extends Role { 13 | private List resourceList; 14 | } 15 | -------------------------------------------------------------------------------- /demo-frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /demo-frontend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /demo-frontend/src/app/shared/guard/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthGuard } from './auth.guard'; 4 | 5 | describe('AuthGuard', () => { 6 | let guard: AuthGuard; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | guard = TestBed.inject(AuthGuard); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(guard).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /demo-frontend/src/app/shared/guard/login.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestBed} from '@angular/core/testing'; 2 | 3 | import {LoginGuard} from './login.guard'; 4 | 5 | describe('LoginGuard', () => { 6 | let guard: LoginGuard; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | guard = TestBed.inject(LoginGuard); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(guard).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /resource-server/src/main/java/com/example/ResourceServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ResourceServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ResourceServiceApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/user/bean/UserChangePasswordVO.java: -------------------------------------------------------------------------------- 1 | package com.example.user.bean; 2 | 3 | import com.example.user.valid.ValidToken; 4 | import lombok.Data; 5 | 6 | import javax.validation.constraints.NotBlank; 7 | 8 | @Data 9 | public class UserChangePasswordVO { 10 | @NotBlank(message = "token不能为空") 11 | @ValidToken 12 | private String token; 13 | @NotBlank(message = "密码不能为空") 14 | private String password; 15 | } 16 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/AuthorizationServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class AuthorizationServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(AuthorizationServiceApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /demo-frontend/src/app/shared/common/common.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestBed} from '@angular/core/testing'; 2 | 3 | import {CommonService} from './common.service'; 4 | 5 | describe('CommonService', () => { 6 | let service: CommonService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(CommonService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /resource-server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | mybatis: 2 | mapper-locations: classpath*:mapper/*.xml 3 | security: 4 | oauth2: 5 | client: 6 | client-id: client 7 | client-secret: secret 8 | resource: 9 | token-info-uri: http://localhost:8080/oauth/check_token 10 | spring: 11 | jackson: 12 | serialization: 13 | FAIL_ON_EMPTY_BEANS: false 14 | server: 15 | port: 8081 16 | error: 17 | include-binding-errors: always 18 | include-message: always -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/user/bean/UserRegisterVO.java: -------------------------------------------------------------------------------- 1 | package com.example.user.bean; 2 | 3 | import com.example.user.valid.ValidUsername; 4 | import lombok.Data; 5 | 6 | import javax.validation.constraints.NotBlank; 7 | 8 | /** 9 | * 注册用户时使用的vo 10 | */ 11 | @Data 12 | public class UserRegisterVO { 13 | @NotBlank(message = "用户名不能为空") 14 | @ValidUsername 15 | private String username; 16 | @NotBlank(message = "密码不能为空") 17 | private String password; 18 | } 19 | -------------------------------------------------------------------------------- /demo-frontend/src/app/shared/pipe/gender.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'gender' 5 | }) 6 | export class GenderPipe implements PipeTransform { 7 | 8 | transform(value: unknown, ...args: unknown[]): unknown { 9 | if (value === 1) { 10 | return '男' 11 | } else if (value === 2) { 12 | return '女' 13 | } else if (value === 3) { 14 | return '其它' 15 | } else { 16 | return '' 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /demo-frontend/src/app/shared/common/auth.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestBed} from '@angular/core/testing'; 2 | 3 | import {AuthInterceptor} from './auth.interceptor'; 4 | 5 | describe('AuthInterceptor', () => { 6 | beforeEach(() => TestBed.configureTestingModule({ 7 | providers: [ 8 | AuthInterceptor 9 | ] 10 | })); 11 | 12 | it('should be created', () => { 13 | const interceptor: AuthInterceptor = TestBed.inject(AuthInterceptor); 14 | expect(interceptor).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/generator/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.generator.mapper; 2 | 3 | import com.example.generator.model.User; 4 | import com.example.user.bean.UserVO; 5 | import tk.mybatis.mapper.common.Mapper; 6 | 7 | import java.util.List; 8 | 9 | public interface UserMapper extends Mapper { 10 | List findAllFetchRoleAndResource(String username); 11 | 12 | void initRoleByUserId(Integer id); 13 | 14 | void changePassword(String username, String password); 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /demo-frontend/src/app/shared/icons-provider.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {NZ_ICONS, NzIconModule} from 'ng-zorro-antd/icon'; 3 | 4 | import {DashboardOutline, FormOutline, MenuFoldOutline, MenuUnfoldOutline} from '@ant-design/icons-angular/icons'; 5 | 6 | const icons = [MenuFoldOutline, MenuUnfoldOutline, DashboardOutline, FormOutline]; 7 | 8 | @NgModule({ 9 | imports: [NzIconModule], 10 | exports: [NzIconModule], 11 | providers: [ 12 | {provide: NZ_ICONS, useValue: icons} 13 | ] 14 | }) 15 | export class IconsProviderModule { 16 | } 17 | -------------------------------------------------------------------------------- /resource-server/src/main/java/com/example/user/vo/UserDetailVO.java: -------------------------------------------------------------------------------- 1 | package com.example.user.vo; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class UserDetailVO { 7 | private Integer id; 8 | /** 9 | * 关联t_user表id 10 | */ 11 | private Integer userId; 12 | 13 | /** 14 | * 用户姓名 15 | */ 16 | private String name; 17 | 18 | /** 19 | * 年龄 20 | */ 21 | private Integer age; 22 | 23 | /** 24 | * 电子邮箱 25 | */ 26 | private String email; 27 | 28 | /** 29 | * 性别,1为男性,2为女性,3为其它 30 | */ 31 | private Integer gender; 32 | } 33 | -------------------------------------------------------------------------------- /authorization-server/src/main/resources/mapper/RoleMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resource-server/src/main/java/com/example/config/ResourceConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 5 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 6 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 7 | 8 | @Configuration 9 | @EnableResourceServer 10 | @EnableGlobalMethodSecurity(prePostEnabled = true) 11 | public class ResourceConfig extends ResourceServerConfigurerAdapter { 12 | } 13 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-personal-role/show-personal-role.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {HttpClient} from "@angular/common/http"; 3 | 4 | @Component({ 5 | selector: 'app-show-personal-role', 6 | templateUrl: './show-personal-role.component.html', 7 | styleUrls: ['./show-personal-role.component.scss'] 8 | }) 9 | export class ShowPersonalRoleComponent implements OnInit { 10 | list: any = [] 11 | 12 | constructor( 13 | private http: HttpClient, 14 | ) { 15 | } 16 | 17 | ngOnInit(): void { 18 | this.http.get(`/user/showPersonalRole`).subscribe(r => { 19 | this.list = r 20 | }) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /authorization-server/src/main/resources/mapper/ResourceMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo-frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /resource-server/src/main/java/com/example/generator/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.example.generator.mapper; 2 | 3 | import com.example.generator.model.User; 4 | import com.example.user.vo.UserDetailVO; 5 | import com.example.user.vo.UserVO; 6 | import org.apache.ibatis.annotations.Param; 7 | import tk.mybatis.mapper.common.Mapper; 8 | 9 | import java.util.List; 10 | 11 | public interface UserMapper extends Mapper { 12 | List selectAllFetchDetail(Integer id); 13 | 14 | List showPersonalRole(String username); 15 | 16 | UserDetailVO showPersonalDetail(String username); 17 | 18 | void updatePersonalDetail(@Param("item") UserDetailVO userDetailVO); 19 | } -------------------------------------------------------------------------------- /demo-frontend/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestBed} from '@angular/core/testing'; 2 | import {RouterTestingModule} from '@angular/router/testing'; 3 | import {AppComponent} from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /authorization-server/src/test/resources/application.yaml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | com.example: info 4 | # org.apache.http: debug 5 | # org.springframework.security: debug 6 | pattern: 7 | # console: "%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}" 8 | console: "%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}" 9 | 10 | --- 11 | spring: 12 | profiles: 13 | active: unit-test 14 | sql: 15 | init: 16 | schema-locations: classpath*:sql/unit-test-schema.sql 17 | data-locations: classpath*:sql/unit-test-data.sql -------------------------------------------------------------------------------- /resource-server/src/test/resources/sql/unit-test-data.sql: -------------------------------------------------------------------------------- 1 | insert into t_user (id, username, password) 2 | values (1, 'user', '$2a$10$UYfUeE54zgz3eb5sMDt2B.I7DZhXQxlboFHAkQjMGe3grcAmWXQCa'), 3 | (2, 'admin', '$2a$10$P05jOa7SIFEPf58XrPk1euoFcAgzLavabdLUXd8uFIrHZ/pflZr02'); 4 | insert into t_user_detail (id, user_id, name, age, email, gender) 5 | values (1, 1, '测试用户', 12, 'test@test.com', 1); 6 | insert into t_user_detail (id, user_id, name, age, email, gender) 7 | values (2, 2, '管理员用户', 66, 'admin@test.com', 2); 8 | insert into t_role (id, name, description) 9 | values (1, 'user', '普通用户'), 10 | (2, 'admin', '系统管理员') 11 | ; 12 | insert into r_user_role (user_id, role_id) 13 | values (1, 1), 14 | (2, 1), 15 | (2, 2) 16 | ; -------------------------------------------------------------------------------- /demo-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/index/index.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {IndexComponent} from './index.component'; 4 | 5 | describe('IndexComponent', () => { 6 | let component: IndexComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [IndexComponent] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(IndexComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-user-list/show-user-list.component.html: -------------------------------------------------------------------------------- 1 |

用户列表

2 | 3 | 4 | 5 | 用户名 6 | 姓名 7 | 年龄 8 | 性别 9 | 电子邮箱 10 | 操作 11 | 12 | 13 | 14 | 15 | {{data.username}} 16 | {{data.name}} 17 | {{data.age}} 18 | {{data.gender | gender}} 19 | {{data.email}} 20 | 21 | 查看详情 22 | 23 | 重置密码 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ LoginComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoginComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/generator/model/Role.java: -------------------------------------------------------------------------------- 1 | package com.example.generator.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | 11 | /** 12 | * 表名:t_role 13 | * 表注释:角色表 14 | */ 15 | @Getter 16 | @Setter 17 | @Table(name = "t_role") 18 | public class Role { 19 | @Id 20 | @Column(name = "id") 21 | @GeneratedValue(generator = "JDBC") 22 | private Integer id; 23 | 24 | /** 25 | * 角色名 26 | */ 27 | @Column(name = "name") 28 | private String name; 29 | 30 | /** 31 | * 角色描述 32 | */ 33 | @Column(name = "description") 34 | private String description; 35 | } -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/register/register.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {RegisterComponent} from './register.component'; 4 | 5 | describe('RegisterComponent', () => { 6 | let component: RegisterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [RegisterComponent] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RegisterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-personal-detail/show-personal-detail.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {HttpClient} from "@angular/common/http"; 3 | 4 | @Component({ 5 | selector: 'app-show-personal-detail', 6 | templateUrl: './show-personal-detail.component.html', 7 | styleUrls: ['./show-personal-detail.component.scss'] 8 | }) 9 | export class ShowPersonalDetailComponent implements OnInit { 10 | details: any = { 11 | name: undefined, 12 | age: undefined, 13 | email: undefined, 14 | gender: undefined, 15 | } 16 | 17 | constructor( 18 | private http: HttpClient, 19 | ) { 20 | } 21 | 22 | ngOnInit(): void { 23 | this.http.get(`/user/showPersonalDetail`).subscribe(r => { 24 | this.details = r 25 | }) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/generator/model/Resource.java: -------------------------------------------------------------------------------- 1 | package com.example.generator.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | 11 | /** 12 | * 表名:t_resource 13 | * 表注释:资源表 14 | */ 15 | @Getter 16 | @Setter 17 | @Table(name = "t_resource") 18 | public class Resource { 19 | @Id 20 | @Column(name = "id") 21 | @GeneratedValue(generator = "JDBC") 22 | private Integer id; 23 | 24 | /** 25 | * 资源名,唯一,英文,尽量短 26 | */ 27 | @Column(name = "name") 28 | private String name; 29 | 30 | /** 31 | * 资源描述 32 | */ 33 | @Column(name = "description") 34 | private String description; 35 | } -------------------------------------------------------------------------------- /resource-server/src/test/java/com/example/config/TestResourceConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.config; 2 | 3 | import org.springframework.boot.test.context.TestConfiguration; 4 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 5 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 6 | import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; 7 | 8 | @TestConfiguration 9 | public class TestResourceConfig extends ResourceServerConfigurerAdapter { 10 | @Override 11 | public void configure(HttpSecurity http) { 12 | } 13 | 14 | @Override 15 | public void configure(ResourceServerSecurityConfigurer resources) { 16 | resources.stateless(false); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo-frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | oauthUrl: 'http://localhost:8080', 8 | api: 'http://localhost:8081' 9 | }; 10 | 11 | /* 12 | * For easier debugging in development mode, you can import the following file 13 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 14 | * 15 | * This import should be commented out in production mode because it will have a negative impact 16 | * on performance if an error is thrown. 17 | */ 18 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 19 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-user-list/show-user-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ShowUserListComponent } from './show-user-list.component'; 4 | 5 | describe('ShowUserListComponent', () => { 6 | let component: ShowUserListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ShowUserListComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ShowUserListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /resource-server/src/test/resources/application.yaml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | com.example: debug 4 | # org.apache.http: debug 5 | # org.springframework.security: debug 6 | pattern: 7 | # console: "%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}" 8 | console: "%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}" 9 | 10 | --- 11 | spring: 12 | profiles: unit-test 13 | sql: 14 | init: 15 | schema-locations: classpath*:sql/unit-test-schema.sql 16 | data-locations: classpath*:sql/unit-test-data.sql 17 | 18 | --- 19 | spring: 20 | profiles: unit-test-mysql-mode 21 | datasource: 22 | url: jdbc:h2:mem:testdb;MODE=MySQL -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-user-detail/show-user-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ShowUserDetailComponent } from './show-user-detail.component'; 4 | 5 | describe('ShowUserDetailComponent', () => { 6 | let component: ShowUserDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ShowUserDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ShowUserDetailComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /demo-frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | (id: string): T; 13 | keys(): string[]; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting(), 21 | ); 22 | 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().map(context); 27 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-personal-role/show-personal-role.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ShowPersonalRoleComponent } from './show-personal-role.component'; 4 | 5 | describe('ShowPersonalRoleComponent', () => { 6 | let component: ShowPersonalRoleComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ShowPersonalRoleComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ShowPersonalRoleComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/reset-user-password/reset-user-password.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ResetUserPasswordComponent } from './reset-user-password.component'; 4 | 5 | describe('ResetUserPasswordComponent', () => { 6 | let component: ResetUserPasswordComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ResetUserPasswordComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ResetUserPasswordComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /resource-server/src/main/resources/mapper/UserDetailMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-personal-detail/show-personal-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ShowPersonalDetailComponent } from './show-personal-detail.component'; 4 | 5 | describe('ShowPersonalDetailComponent', () => { 6 | let component: ShowPersonalDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ShowPersonalDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ShowPersonalDetailComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/update-personal-detail/update-personal-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UpdatePersonalDetailComponent } from './update-personal-detail.component'; 4 | 5 | describe('UpdatePersonalDetailComponent', () => { 6 | let component: UpdatePersonalDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ UpdatePersonalDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(UpdatePersonalDetailComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/login/login.component.html: -------------------------------------------------------------------------------- 1 |

登录

2 | 23 | -------------------------------------------------------------------------------- /resource-server/src/main/java/com/example/generator/model/User.java: -------------------------------------------------------------------------------- 1 | package com.example.generator.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | 11 | /** 12 | * 表名:t_user 13 | * 表注释:用户表 14 | */ 15 | @Getter 16 | @Setter 17 | @Table(name = "t_user") 18 | public class User { 19 | @Id 20 | @Column(name = "id") 21 | @GeneratedValue(generator = "JDBC") 22 | private Integer id; 23 | 24 | /** 25 | * 用户名 26 | */ 27 | @Column(name = "username") 28 | private String username; 29 | 30 | /** 31 | * 密码,根据需求加密后存储 32 | */ 33 | @Column(name = "password") 34 | private String password; 35 | 36 | /** 37 | * 是否可用,1为可用,0为不可用,默认1可用 38 | */ 39 | @Column(name = "enable") 40 | private Boolean enable; 41 | } -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/generator/model/User.java: -------------------------------------------------------------------------------- 1 | package com.example.generator.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | 11 | /** 12 | * 表名:t_user 13 | * 表注释:用户表 14 | */ 15 | @Getter 16 | @Setter 17 | @Table(name = "t_user") 18 | public class User { 19 | @Id 20 | @Column(name = "id") 21 | @GeneratedValue(generator = "JDBC") 22 | private Integer id; 23 | 24 | /** 25 | * 用户名 26 | */ 27 | @Column(name = "username") 28 | private String username; 29 | 30 | /** 31 | * 密码,根据需求加密后存储 32 | */ 33 | @Column(name = "password") 34 | private String password; 35 | 36 | /** 37 | * 是否可用,1为可用,0为不可用,默认1可用 38 | */ 39 | @Column(name = "enable") 40 | private Boolean enable; 41 | } -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-user-list/show-user-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {HttpClient} from "@angular/common/http"; 3 | import {Router} from "@angular/router"; 4 | 5 | @Component({ 6 | selector: 'app-show-user-list', 7 | templateUrl: './show-user-list.component.html', 8 | styleUrls: ['./show-user-list.component.scss'] 9 | }) 10 | export class ShowUserListComponent implements OnInit { 11 | dataSet: any = [] 12 | 13 | constructor( 14 | private http: HttpClient, 15 | private router: Router) { 16 | } 17 | 18 | ngOnInit(): void { 19 | this.load() 20 | } 21 | 22 | load() { 23 | this.http.get(`/user`).subscribe(r => { 24 | this.dataSet = r 25 | }) 26 | } 27 | 28 | details(data: any) { 29 | this.router.navigate(['/showUserDetail'], {queryParams: {id: data.id}}) 30 | } 31 | 32 | changePassword(data: any) { 33 | this.router.navigate(['/resetUserPassword'], {queryParams: {id: data.id}}) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /demo-frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "es2017", 20 | "module": "es2020", 21 | "lib": [ 22 | "es2020", 23 | "dom" 24 | ] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/user/UserController.java: -------------------------------------------------------------------------------- 1 | package com.example.user; 2 | 3 | import com.example.user.bean.UserChangePasswordVO; 4 | import com.example.user.bean.UserRegisterVO; 5 | import org.springframework.validation.annotation.Validated; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import javax.annotation.Resource; 11 | 12 | @RestController 13 | public class UserController { 14 | @Resource 15 | UserService userService; 16 | 17 | /** 18 | * 用户注册 19 | */ 20 | @PostMapping("/oauth/register") 21 | public void register(@RequestBody @Validated UserRegisterVO userRegisterVO) { 22 | userService.register(userRegisterVO); 23 | } 24 | 25 | @PostMapping("/oauth/changePassword") 26 | public void changePassword(@RequestBody @Validated UserChangePasswordVO userChangePasswordVO) { 27 | userService.changePassword(userChangePasswordVO); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resource-server/src/main/java/com/example/config/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 7 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 8 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 9 | import org.springframework.security.crypto.password.PasswordEncoder; 10 | 11 | @Configuration 12 | @EnableWebSecurity 13 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 14 | @Bean 15 | public PasswordEncoder passwordEncoder() { 16 | return new BCryptPasswordEncoder(); 17 | } 18 | 19 | @Override 20 | protected void configure(HttpSecurity http) throws Exception { 21 | super.configure(http); 22 | http.csrf().disable(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/client/ClientService.java: -------------------------------------------------------------------------------- 1 | package com.example.client; 2 | 3 | import com.example.generator.mapper.ClientMapper; 4 | import com.example.generator.model.Client; 5 | import org.springframework.security.oauth2.provider.ClientDetails; 6 | import org.springframework.security.oauth2.provider.ClientDetailsService; 7 | import org.springframework.security.oauth2.provider.ClientRegistrationException; 8 | import org.springframework.stereotype.Service; 9 | import tk.mybatis.mapper.entity.Example; 10 | 11 | import javax.annotation.Resource; 12 | 13 | @Service 14 | public class ClientService implements ClientDetailsService { 15 | @Resource 16 | ClientMapper clientMapper; 17 | 18 | @Override 19 | public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException { 20 | Example example = new Example(Client.class); 21 | example.createCriteria().andEqualTo("clientId", clientId); 22 | Client client = clientMapper.selectOneByExample(example); 23 | return ClientVO.from(client); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.example 6 | spring-boot-oauth2-example 7 | 0.0.1-SNAPSHOT 8 | pom 9 | PARENT::ROOT 10 | 11 | 12 | authorization-server 13 | resource-server 14 | 15 | 16 | 17 | 18 | aliyun 19 | https://maven.aliyun.com/repository/central 20 | 21 | 22 | 23 | 24 | aliyun 25 | https://maven.aliyun.com/repository/central 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /authorization-server/src/test/java/com/example/user/UserServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.example.user; 2 | 3 | import com.example.generator.mapper.UserMapper; 4 | import com.example.user.bean.UserVO; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.InjectMocks; 9 | import org.mockito.Mock; 10 | import org.mockito.Mockito; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | 13 | import java.util.List; 14 | 15 | @ExtendWith(MockitoExtension.class) 16 | class UserServiceTest { 17 | 18 | @InjectMocks 19 | UserService userService; 20 | @Mock 21 | UserMapper userMapper; 22 | 23 | @Test 24 | void findAllFetchRoleAndResource() { 25 | UserVO userVO = new UserVO(); 26 | Mockito.when(userMapper.findAllFetchRoleAndResource(null)).thenReturn(List.of(userVO)); 27 | 28 | List users = userService.findAllFetchRoleAndResource(); 29 | Assertions.assertNotNull(users); 30 | Assertions.assertFalse(users.isEmpty()); 31 | Assertions.assertEquals(1, users.size()); 32 | } 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 大胃王 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {FormBuilder, FormGroup, Validators} from "@angular/forms"; 3 | import {CommonService} from "../../shared/common/common.service"; 4 | import {Router} from "@angular/router"; 5 | 6 | @Component({ 7 | selector: 'app-login', 8 | templateUrl: './login.component.html', 9 | styleUrls: ['./login.component.scss'] 10 | }) 11 | export class LoginComponent implements OnInit { 12 | validateForm: FormGroup; 13 | 14 | constructor( 15 | private fb: FormBuilder, 16 | private service: CommonService, 17 | private router: Router, 18 | ) { 19 | this.validateForm = this.fb.group({ 20 | username: [null, Validators.required], 21 | password: [null, Validators.required], 22 | }) 23 | } 24 | 25 | ngOnInit(): void { 26 | } 27 | 28 | submitForm() { 29 | let rawValue = this.validateForm.getRawValue(); 30 | this.service.login(rawValue.username, rawValue.password).subscribe(r => { 31 | this.service.token = r 32 | this.service.success(`登录成功`) 33 | this.router.navigate(['/index']) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /resource-server/src/main/java/com/example/config/ValidatorConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.config; 2 | 3 | import org.hibernate.validator.HibernateValidator; 4 | import org.springframework.beans.factory.config.AutowireCapableBeanFactory; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; 8 | 9 | import javax.validation.Validation; 10 | import javax.validation.Validator; 11 | 12 | /** 13 | * 校验相关配置 14 | */ 15 | @Configuration 16 | public class ValidatorConfig { 17 | @Bean 18 | public Validator validator(AutowireCapableBeanFactory autowireCapableBeanFactory) { 19 | return Validation.byProvider(HibernateValidator.class) 20 | .configure() 21 | // 在ConstraintValidator实现类中允许注入spring bean 22 | .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) 23 | // 在校验到错误后立刻返回错误信息,而不是全部都进行校验 24 | .failFast(true) 25 | .buildValidatorFactory().getValidator(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resource-server/src/main/java/com/example/generator/model/UserDetail.java: -------------------------------------------------------------------------------- 1 | package com.example.generator.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | 11 | /** 12 | * 表名:t_user_detail 13 | * 表注释:用户详情表 14 | */ 15 | @Getter 16 | @Setter 17 | @Table(name = "t_user_detail") 18 | public class UserDetail { 19 | @Id 20 | @Column(name = "id") 21 | @GeneratedValue(generator = "JDBC") 22 | private Integer id; 23 | 24 | /** 25 | * 关联t_user表id 26 | */ 27 | @Column(name = "user_id") 28 | private Integer userId; 29 | 30 | /** 31 | * 用户姓名 32 | */ 33 | @Column(name = "name") 34 | private String name; 35 | 36 | /** 37 | * 年龄 38 | */ 39 | @Column(name = "age") 40 | private Integer age; 41 | 42 | /** 43 | * 电子邮箱 44 | */ 45 | @Column(name = "email") 46 | private String email; 47 | 48 | /** 49 | * 性别,1为男性,2为女性,3为其它 50 | */ 51 | @Column(name = "gender") 52 | private Integer gender; 53 | } -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/config/ValidatorConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.config; 2 | 3 | import org.hibernate.validator.HibernateValidator; 4 | import org.springframework.beans.factory.config.AutowireCapableBeanFactory; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; 8 | 9 | import javax.validation.Validation; 10 | import javax.validation.Validator; 11 | 12 | /** 13 | * 校验相关配置 14 | */ 15 | @Configuration 16 | public class ValidatorConfig { 17 | @Bean 18 | public Validator validator(AutowireCapableBeanFactory autowireCapableBeanFactory) { 19 | return Validation.byProvider(HibernateValidator.class) 20 | .configure() 21 | // 在ConstraintValidator实现类中允许注入spring bean 22 | .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) 23 | // 在校验到错误后立刻返回错误信息,而不是全部都进行校验 24 | .failFast(true) 25 | .buildValidatorFactory().getValidator(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /demo-frontend/README.md: -------------------------------------------------------------------------------- 1 | # DemoFrontend 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.2.5. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /authorization-server/src/test/resources/sql/unit-test-data.sql: -------------------------------------------------------------------------------- 1 | insert into t_client (id, client_id, client_secret, registered_redirect_uri_str) 2 | values (1, 'client', '$2a$10$4exGEs2hdi0C5C8wiDzirOft5WLC7/WJFXVqz7cXhVw6/tnFOstZi', 'http://localhost:4200/login'); 3 | insert into t_user (id, username, password) 4 | values (1, 'user', '$2a$10$UYfUeE54zgz3eb5sMDt2B.I7DZhXQxlboFHAkQjMGe3grcAmWXQCa'), 5 | (2, 'admin', '$2a$10$P05jOa7SIFEPf58XrPk1euoFcAgzLavabdLUXd8uFIrHZ/pflZr02'); 6 | insert into t_role (id, name, description) 7 | values (1, 'user', '普通用户'), 8 | (2, 'admin', '系统管理员') 9 | ; 10 | insert into r_user_role (user_id, role_id) 11 | values (1, 1), 12 | (2, 1), 13 | (2, 2) 14 | ; 15 | insert into t_resource (id, name, description) 16 | values (1, 'showPersonalRole', '查看个人角色'), 17 | (2, 'updatePersonalDetail', '编辑个人信息'), 18 | (3, 'showUserList', '查看用户列表'), 19 | (4, 'showUserDetail', '查看用户详情'), 20 | (5, 'resetUserPassword', '重置用户密码'), 21 | (6, 'showPersonalDetail', '查看个人信息') 22 | ; 23 | insert into r_role_resource (role_id, resource_id) 24 | values (1, 1), 25 | (1, 2), 26 | (2, 3), 27 | (2, 4), 28 | (2, 5), 29 | (1, 6) 30 | ; -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-user-detail/show-user-detail.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {ActivatedRoute, Router} from "@angular/router"; 3 | import {CommonService} from "../../shared/common/common.service"; 4 | import {HttpClient} from "@angular/common/http"; 5 | 6 | @Component({ 7 | selector: 'app-show-user-detail', 8 | templateUrl: './show-user-detail.component.html', 9 | styleUrls: ['./show-user-detail.component.scss'] 10 | }) 11 | export class ShowUserDetailComponent implements OnInit { 12 | details: any = { 13 | name: undefined, 14 | age: undefined, 15 | email: undefined, 16 | gender: undefined, 17 | } 18 | 19 | constructor( 20 | private route: ActivatedRoute, 21 | private router: Router, 22 | private service: CommonService, 23 | private http: HttpClient, 24 | ) { 25 | } 26 | 27 | ngOnInit(): void { 28 | this.route.queryParams.subscribe(r => { 29 | if (!r['id']) { 30 | this.service.info(`请先选择用户`) 31 | this.router.navigate(['/showUserList']) 32 | } else { 33 | this.http.get(`/user/details/${r['id']}`).subscribe(r1 => { 34 | this.details = r1 35 | }) 36 | } 37 | }) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /resource-server/src/test/resources/sql/unit-test-schema.sql: -------------------------------------------------------------------------------- 1 | create schema `spring-boot-oauth2-example`; 2 | 3 | create table t_user 4 | ( 5 | id int auto_increment primary key, 6 | username varchar(100) not null comment '用户名', 7 | password varchar(100) not null comment '密码,根据需求加密后存储', 8 | enable bit default 1 not null comment '是否可用,1为可用,0为不可用,默认1可用', 9 | constraint t_user_username_uindex unique (username) 10 | ); 11 | 12 | create table t_user_detail 13 | ( 14 | id int auto_increment primary key, 15 | user_id int not null comment '关联t_user表id', 16 | name varchar(20) null comment '用户姓名', 17 | age int null comment '年龄', 18 | email varchar(100) null comment '电子邮箱', 19 | gender int null comment '性别,1为男性,2为女性,3为其它', 20 | constraint t_user_detail_user_id_uindex unique (user_id) 21 | ); 22 | 23 | create table t_role 24 | ( 25 | id int auto_increment primary key, 26 | name varchar(200) not null comment '角色名', 27 | description varchar(200) not null comment '角色描述', 28 | constraint t_role_name_uindex unique (name) 29 | ); 30 | 31 | create table r_user_role 32 | ( 33 | user_id int not null, 34 | role_id int not null, 35 | primary key (user_id, role_id) 36 | ); 37 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/update-personal-detail/update-personal-detail.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {HttpClient} from "@angular/common/http"; 3 | import {FormBuilder, FormGroup} from "@angular/forms"; 4 | import {CommonService} from "../../shared/common/common.service"; 5 | 6 | @Component({ 7 | selector: 'app-update-personal-detail', 8 | templateUrl: './update-personal-detail.component.html', 9 | styleUrls: ['./update-personal-detail.component.scss'] 10 | }) 11 | export class UpdatePersonalDetailComponent implements OnInit { 12 | validateForm: FormGroup 13 | 14 | constructor( 15 | private http: HttpClient, 16 | private fb: FormBuilder, 17 | private service: CommonService, 18 | ) { 19 | this.validateForm = this.fb.group({ 20 | id: [], 21 | userId: [], 22 | name: [], 23 | age: [], 24 | email: [], 25 | gender: [], 26 | }) 27 | } 28 | 29 | ngOnInit(): void { 30 | this.http.get(`/user/showPersonalDetail`).subscribe(r => { 31 | this.validateForm.patchValue(r) 32 | }) 33 | } 34 | 35 | submitForm() { 36 | this.http.post(`/user/updatePersonalDetail`, this.validateForm.getRawValue()).subscribe(() => { 37 | this.service.success(`保存成功`) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /demo-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-frontend", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --proxy-config proxy.json", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "~13.2.0", 14 | "@angular/common": "~13.2.0", 15 | "@angular/compiler": "~13.2.0", 16 | "@angular/core": "~13.2.0", 17 | "@angular/forms": "~13.2.0", 18 | "@angular/platform-browser": "~13.2.0", 19 | "@angular/platform-browser-dynamic": "~13.2.0", 20 | "@angular/router": "~13.2.0", 21 | "ng-zorro-antd": "^13.1.1", 22 | "rxjs": "~7.5.0", 23 | "tslib": "^2.3.0", 24 | "zone.js": "~0.11.4" 25 | }, 26 | "devDependencies": { 27 | "@angular-devkit/build-angular": "~13.2.5", 28 | "@angular/cli": "~13.2.5", 29 | "@angular/compiler-cli": "~13.2.0", 30 | "@types/jasmine": "~3.10.0", 31 | "@types/node": "^12.11.1", 32 | "jasmine-core": "~4.0.0", 33 | "karma": "~6.3.0", 34 | "karma-chrome-launcher": "~3.1.0", 35 | "karma-coverage": "~2.1.0", 36 | "karma-jasmine": "~4.0.0", 37 | "karma-jasmine-html-reporter": "~1.7.0", 38 | "typescript": "~4.5.2" 39 | } 40 | } -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/user/valid/ValidUsername.java: -------------------------------------------------------------------------------- 1 | package com.example.user.valid; 2 | 3 | import com.example.user.UserService; 4 | 5 | import javax.annotation.Resource; 6 | import javax.validation.Constraint; 7 | import javax.validation.ConstraintValidator; 8 | import javax.validation.ConstraintValidatorContext; 9 | import javax.validation.Payload; 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.RetentionPolicy; 12 | import java.lang.annotation.Target; 13 | 14 | import static java.lang.annotation.ElementType.*; 15 | 16 | /** 17 | * 校验用户名是否存在,在注册用户之前使用 18 | */ 19 | @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) 20 | @Retention(RetentionPolicy.RUNTIME) 21 | @Constraint(validatedBy = ValidUsername.UsernameValidator.class) 22 | public @interface ValidUsername { 23 | String message() default "用户名已存在"; 24 | 25 | Class[] groups() default {}; 26 | 27 | Class[] payload() default {}; 28 | 29 | class UsernameValidator implements ConstraintValidator { 30 | @Resource 31 | UserService userService; 32 | 33 | @Override 34 | public boolean isValid(String value, ConstraintValidatorContext context) { 35 | return userService.selectUserByUsername(value) == null; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular+SpringBoot+OAuth2授权集成的示例项目 2 | 3 | ## 主要功能 4 | 5 | ### 授权服务器(权限中心) 6 | 7 | - [x] 注册账号 8 | - [x] 通过授权码的方式登录(单点登录),并获取授权码 9 | - [x] 通过授权码获取token 10 | - [x] 通过密码方式登录,并获取token 11 | - [x] 修改密码 12 | - [x] 注销账号 13 | - [x] 通过check_token接口获取用户权限(JWT模式用的比较少) 14 | 15 | ### 资源服务器(客户端,负责具体业务) 16 | 17 | 普通用户 18 | 19 | - [x] 查看个人角色 20 | - [x] 查看个人信息 21 | - [x] 编辑个人信息 22 | 23 | 管理员用户 24 | 25 | - [x] 查看用户列表 26 | - [x] 查看用户详情 27 | - [x] 重置用户密码 28 | 29 | ## 截图展示 30 | 31 | ![注册用户](docs/img/register.png) 32 | 33 | ![修改密码](docs/img/changepassword.png) 34 | 35 | ![用户列表](docs/img/userlist.png) 36 | 37 | ![用户详情](docs/img/userdetail.png) 38 | 39 | ## 文件结构 40 | 41 | ![文件目录结构0](docs/img/文件目录结构0.png) 42 | 43 | ![文件目录结构1](docs/img/文件目录结构1.png) 44 | 45 | ![文件目录结构2](docs/img/文件目录结构2.png) 46 | 47 | ![文件目录结构3](docs/img/文件目录结构3.png) 48 | 49 | ## 授权和资源服务器的集成以及分离 50 | 51 | 本项目是授权和资源服务器的分离示例。 52 | 53 | 集成示例可以参考: https://gitee.com/consolelog/spring-boot-oauth2-single-service-example 54 | 55 | git地址: https://gitee.com/consolelog/spring-boot-oauth2-single-service-example.git 56 | 57 | ## 参考文档 58 | 59 | https://projects.spring.io/spring-security-oauth/docs/oauth2.html 60 | 61 | https://docs.spring.io/spring-security-oauth2-boot/docs/2.6.x/reference/html5/ 62 | 63 | https://consolelog.gitee.io/docs-oauth2/ 64 | 65 | https://docs.spring.io/spring-security/reference/index.html 66 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/user/valid/ValidToken.java: -------------------------------------------------------------------------------- 1 | package com.example.user.valid; 2 | 3 | import org.springframework.security.oauth2.provider.token.TokenStore; 4 | 5 | import javax.annotation.Resource; 6 | import javax.validation.Constraint; 7 | import javax.validation.ConstraintValidator; 8 | import javax.validation.ConstraintValidatorContext; 9 | import javax.validation.Payload; 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.RetentionPolicy; 12 | import java.lang.annotation.Target; 13 | 14 | import static java.lang.annotation.ElementType.*; 15 | 16 | /** 17 | * 校验token是否存在,在修改密码之前使用 18 | */ 19 | @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) 20 | @Retention(RetentionPolicy.RUNTIME) 21 | @Constraint(validatedBy = ValidToken.TokenValidator.class) 22 | public @interface ValidToken { 23 | String message() default "token无法识别"; 24 | 25 | Class[] groups() default {}; 26 | 27 | Class[] payload() default {}; 28 | 29 | class TokenValidator implements ConstraintValidator { 30 | @Resource 31 | TokenStore tokenStore; 32 | 33 | @Override 34 | public boolean isValid(String value, ConstraintValidatorContext context) { 35 | return tokenStore.readAuthentication(value) != null; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /demo-frontend/src/app/shared/guard/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ActivatedRouteSnapshot, CanActivateChild, Router, RouterStateSnapshot, UrlTree} from '@angular/router'; 3 | import {catchError, map, Observable, throwError} from 'rxjs'; 4 | import {CommonService} from "../common/common.service"; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class AuthGuard implements CanActivateChild { 10 | 11 | constructor( 12 | private service: CommonService, 13 | private router: Router, 14 | ) { 15 | } 16 | 17 | canActivateChild( 18 | route: ActivatedRouteSnapshot, 19 | state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { 20 | let user = this.service.user; 21 | if (user == null || user == {}) { 22 | this.router.navigate(['/index']) 23 | return false 24 | } 25 | if (this.service.expired) { 26 | return this.service.refreshToken().pipe(map(r => { 27 | if (r) { 28 | return true 29 | } else { 30 | this.service.info(`登录已过期,请重新登录`) 31 | this.router.navigate(['/index']) 32 | return false 33 | } 34 | }), catchError(e => { 35 | this.service.error(`登录状态刷新失败,请重新登录`) 36 | this.service.noAuth() 37 | return throwError(`error: ${e.error}`) 38 | })) 39 | } 40 | return true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/reset-user-password/reset-user-password.component.html: -------------------------------------------------------------------------------- 1 |

重置用户密码

2 | 35 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-user-detail/show-user-detail.component.html: -------------------------------------------------------------------------------- 1 |

用户个人信息

2 |
3 | 4 | 姓名 5 | 6 | 7 | 8 | 9 | 10 | 11 | 年龄 12 | 13 | 14 | 15 | 16 | 17 | 18 | 电子邮箱 19 | 20 | 21 | 22 | 23 | 24 | 25 | 性别 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/show-personal-detail/show-personal-detail.component.html: -------------------------------------------------------------------------------- 1 |

您的个人信息

2 |
3 | 4 | 姓名 5 | 6 | 7 | 8 | 9 | 10 | 11 | 年龄 12 | 13 | 14 | 15 | 16 | 17 | 18 | 电子邮箱 19 | 20 | 21 | 22 | 23 | 24 | 25 | 性别 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 | -------------------------------------------------------------------------------- /sql/initial-data.sql: -------------------------------------------------------------------------------- 1 | # client表 2 | # client:secret 3 | insert ignore into t_client (id, client_id, client_secret, registered_redirect_uri_str) 4 | values (1, 'client', '$2a$10$4exGEs2hdi0C5C8wiDzirOft5WLC7/WJFXVqz7cXhVw6/tnFOstZi', 'http://localhost:4200/login'); 5 | #用户表 6 | # user:user 7 | # admin:admin 8 | insert ignore into t_user (id, username, password) 9 | values (1, 'user', '$2a$10$UYfUeE54zgz3eb5sMDt2B.I7DZhXQxlboFHAkQjMGe3grcAmWXQCa'), 10 | (2, 'admin', '$2a$10$P05jOa7SIFEPf58XrPk1euoFcAgzLavabdLUXd8uFIrHZ/pflZr02'); 11 | # 角色表 12 | insert ignore into t_role (id, name, description) 13 | values (1, 'user', '普通用户'), 14 | (2, 'admin', '系统管理员') 15 | ; 16 | # 用户角色关系表 17 | insert ignore into r_user_role (user_id, role_id) 18 | values (1, 1), 19 | (2, 1), 20 | (2, 2) 21 | ; 22 | # 资源表 23 | insert ignore into t_resource (id, name, description) 24 | values (1, 'showPersonalRole', '查看个人角色'), 25 | (2, 'updatePersonalDetail', '编辑个人信息'), 26 | (3, 'showUserList', '查看用户列表'), 27 | (4, 'showUserDetail', '查看用户详情'), 28 | (5, 'resetUserPassword', '重置用户密码'), 29 | (6, 'showPersonalDetail', '查看个人信息') 30 | ; 31 | # 角色资源关系表 32 | insert ignore into r_role_resource (role_id, resource_id) 33 | values (1, 1), 34 | (1, 2), 35 | (2, 3), 36 | (2, 4), 37 | (2, 5), 38 | (1, 6) 39 | ; 40 | insert into t_user_detail (id, user_id, name, age, email, gender) 41 | values (1, 1, '测试用户', 12, 'test@test.com', 1), 42 | (2, 2, '管理员用户', 66, 'admin@test.com', 2); -------------------------------------------------------------------------------- /authorization-server/src/test/java/com/example/user/UserMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.example.user; 2 | 3 | import com.example.config.MyBatisConfig; 4 | import com.example.generator.mapper.UserMapper; 5 | import com.example.user.bean.UserVO; 6 | import org.junit.jupiter.api.Test; 7 | import org.mybatis.spring.boot.test.autoconfigure.MybatisTest; 8 | import org.springframework.boot.autoconfigure.ImportAutoConfiguration; 9 | import org.springframework.context.annotation.Import; 10 | import org.springframework.test.context.ActiveProfiles; 11 | import tk.mybatis.mapper.autoconfigure.MapperAutoConfiguration; 12 | 13 | import javax.annotation.Resource; 14 | import java.util.List; 15 | 16 | import static org.junit.jupiter.api.Assertions.assertEquals; 17 | 18 | @MybatisTest 19 | @Import(MyBatisConfig.class) 20 | @ImportAutoConfiguration(MapperAutoConfiguration.class) 21 | @ActiveProfiles("unit-test") 22 | public class UserMapperTest { 23 | @Resource 24 | UserMapper userMapper; 25 | 26 | @Test 27 | void findAllFetchRoleAndResource() { 28 | List allFetchRoleAndResource = userMapper.findAllFetchRoleAndResource(null); 29 | assertEquals(allFetchRoleAndResource.size(), 2); 30 | assertEquals(allFetchRoleAndResource.get(0).getRoleVOList().size(), 1); 31 | assertEquals(allFetchRoleAndResource.get(1).getRoleVOList().size(), 2); 32 | assertEquals(allFetchRoleAndResource.get(0).getAuthorities().size(), 3); 33 | assertEquals(allFetchRoleAndResource.get(1).getAuthorities().size(), 6); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /demo-frontend/src/app/shared/common/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http'; 3 | import {catchError, mergeMap, Observable, throwError} from 'rxjs'; 4 | import {CommonService} from "./common.service"; 5 | 6 | @Injectable() 7 | export class AuthInterceptor implements HttpInterceptor { 8 | 9 | constructor( 10 | private service: CommonService, 11 | ) { 12 | } 13 | 14 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 15 | if (request.url.startsWith('/oauth/')) { 16 | return next.handle(request); 17 | } 18 | let token = this.service.token 19 | if (token != null && token != {}) { 20 | if (this.service.expired) { 21 | return this.service.refreshToken().pipe(mergeMap(() => { 22 | token = this.service.token 23 | return this.getAuthReq(request, token, next); 24 | }), catchError(e => { 25 | this.service.error(`登录状态刷新失败,请重新登录`) 26 | this.service.noAuth() 27 | return throwError(`error: ${e.error}`) 28 | })) 29 | } else { 30 | return this.getAuthReq(request, token, next); 31 | } 32 | } else { 33 | return next.handle(request); 34 | } 35 | } 36 | 37 | getAuthReq(request: HttpRequest, token: any, next: HttpHandler) { 38 | const authReq = request.clone({ 39 | headers: request.headers.set('Authorization', `${token.token_type} ${token.access_token}`) 40 | }); 41 | return next.handle(authReq); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /authorization-server/src/main/resources/mapper/ClientMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo-frontend/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/demo-frontend'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /resource-server/src/test/java/com/example/user/UserMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.example.user; 2 | 3 | import com.example.config.MyBatisConfig; 4 | import com.example.generator.mapper.UserMapper; 5 | import com.example.user.vo.UserVO; 6 | import org.junit.jupiter.api.Test; 7 | import org.mybatis.spring.boot.test.autoconfigure.MybatisTest; 8 | import org.springframework.boot.autoconfigure.ImportAutoConfiguration; 9 | import org.springframework.context.annotation.Import; 10 | import org.springframework.test.context.ActiveProfiles; 11 | import tk.mybatis.mapper.autoconfigure.MapperAutoConfiguration; 12 | 13 | import javax.annotation.Resource; 14 | import java.util.List; 15 | 16 | import static org.junit.jupiter.api.Assertions.assertEquals; 17 | 18 | @MybatisTest 19 | @Import(MyBatisConfig.class) 20 | @ImportAutoConfiguration(MapperAutoConfiguration.class) 21 | @ActiveProfiles("unit-test") 22 | class UserMapperTest { 23 | @Resource 24 | UserMapper userMapper; 25 | 26 | @Test 27 | void selectAllFetchDetail() { 28 | List userVOS = userMapper.selectAllFetchDetail(null); 29 | assertEquals(2, userVOS.size()); 30 | 31 | List userVOS1 = userMapper.selectAllFetchDetail(1); 32 | assertEquals(1, userVOS1.size()); 33 | UserVO userVO = userVOS1.get(0); 34 | assertEquals(1, userVO.getId()); 35 | assertEquals("user", userVO.getUsername()); 36 | assertEquals(1, userVO.getUserId()); 37 | assertEquals("测试用户", userVO.getName()); 38 | assertEquals(12, userVO.getAge()); 39 | assertEquals("test@test.com", userVO.getEmail()); 40 | assertEquals(1, userVO.getGender()); 41 | } 42 | } -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/update-personal-detail/update-personal-detail.component.html: -------------------------------------------------------------------------------- 1 |

修改个人信息

2 |
3 | 4 | 姓名 5 | 6 | 7 | 8 | 9 | 10 | 11 | 年龄 12 | 13 | 14 | 15 | 16 | 17 | 18 | 电子邮箱 19 | 20 | 21 | 22 | 23 | 24 | 25 | 性别 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 | -------------------------------------------------------------------------------- /demo-frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {CommonService} from "./shared/common/common.service"; 3 | import {environment} from "../environments/environment"; 4 | import {Router} from "@angular/router"; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.scss'] 10 | }) 11 | export class AppComponent implements OnInit { 12 | isCollapsed = false; 13 | 14 | userInfo = { 15 | user_name: '', 16 | authorities: [], 17 | } 18 | 19 | constructor( 20 | private service: CommonService, 21 | private router: Router, 22 | ) { 23 | } 24 | 25 | ngOnInit(): void { 26 | this.userInfo = this.service.user 27 | this.service.user$.subscribe(r => { 28 | this.userInfo = r 29 | }) 30 | } 31 | 32 | sso() { 33 | const redirectUri = "http://localhost:4200/login" 34 | const clientId = 'client' 35 | const state = this.router.url 36 | const parameter = `response_type=code&redirect_uri=${redirectUri}&client_id=${clientId}&state=${state}`; 37 | window.location.href = `${environment.oauthUrl}/oauth/authorize?${parameter}` 38 | } 39 | 40 | logout() { 41 | const redirectUri = "http://localhost:4200" 42 | const token = this.service.token['access_token'] 43 | this.service.clearAuth() 44 | window.location.href = `${environment.oauthUrl}/oauth/logout?token=${token}&redirect=${redirectUri}` 45 | } 46 | 47 | matchAuthority(resource: string) { 48 | if (!this.userInfo || !this.userInfo.authorities) { 49 | return false 50 | } 51 | const authorities = this.userInfo.authorities as string[] 52 | return authorities.indexOf(resource) > -1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /demo-frontend/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | text-rendering: optimizeLegibility; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | } 7 | 8 | .app-layout { 9 | height: 100%; 10 | } 11 | 12 | .menu-sidebar { 13 | position: relative; 14 | z-index: 10; 15 | min-height: 100vh; 16 | box-shadow: 2px 0 6px rgba(0, 21, 41, .35); 17 | } 18 | 19 | .header-trigger { 20 | height: 64px; 21 | padding: 20px 24px; 22 | font-size: 20px; 23 | cursor: pointer; 24 | transition: all .3s, padding 0s; 25 | } 26 | 27 | .trigger:hover { 28 | color: #1890ff; 29 | } 30 | 31 | .sidebar-logo { 32 | position: relative; 33 | height: 64px; 34 | padding-left: 24px; 35 | overflow: hidden; 36 | line-height: 64px; 37 | background: #001529; 38 | transition: all .3s; 39 | } 40 | 41 | .sidebar-logo img { 42 | display: inline-block; 43 | height: 32px; 44 | width: 32px; 45 | vertical-align: middle; 46 | } 47 | 48 | .sidebar-logo h1 { 49 | display: inline-block; 50 | margin: 0 0 0 20px; 51 | color: #fff; 52 | font-weight: 600; 53 | font-size: 14px; 54 | font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif; 55 | vertical-align: middle; 56 | } 57 | 58 | nz-header { 59 | padding: 0; 60 | width: 100%; 61 | z-index: 2; 62 | } 63 | 64 | .app-header { 65 | position: relative; 66 | height: 64px; 67 | padding: 0; 68 | background: #fff; 69 | box-shadow: 0 1px 4px rgba(0, 21, 41, .08); 70 | } 71 | 72 | nz-content { 73 | margin: 24px; 74 | } 75 | 76 | .inner-content { 77 | padding: 24px; 78 | background: #fff; 79 | height: 100%; 80 | } 81 | 82 | .login-div { 83 | display: inline-block; 84 | float: right; 85 | margin-right: 24px; 86 | } 87 | -------------------------------------------------------------------------------- /authorization-server/src/main/resources/generator/generatorConfig.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 |
-------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/user/bean/UserVO.java: -------------------------------------------------------------------------------- 1 | package com.example.user.bean; 2 | 3 | import cn.hutool.core.bean.BeanUtil; 4 | import com.example.generator.model.User; 5 | import lombok.Data; 6 | import lombok.EqualsAndHashCode; 7 | import org.springframework.security.core.GrantedAuthority; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.util.ObjectUtils; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Collection; 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | @EqualsAndHashCode(callSuper = true) 17 | @Data 18 | public class UserVO extends User implements UserDetails { 19 | private List roleVOList; 20 | 21 | public static UserVO from(User user) { 22 | UserVO userVO = new UserVO(); 23 | BeanUtil.copyProperties(user, userVO); 24 | return userVO; 25 | } 26 | 27 | @Override 28 | public Collection getAuthorities() { 29 | if (ObjectUtils.isEmpty(this.roleVOList)) { 30 | return new ArrayList<>(); 31 | } 32 | return this.roleVOList.stream() 33 | .flatMap(s -> s.getResourceList().stream()) 34 | .map(s -> (GrantedAuthority) s::getName) 35 | .collect(Collectors.toList()); 36 | } 37 | 38 | @Override 39 | public boolean isAccountNonExpired() { 40 | return true; 41 | } 42 | 43 | @Override 44 | public boolean isAccountNonLocked() { 45 | return true; 46 | } 47 | 48 | @Override 49 | public boolean isCredentialsNonExpired() { 50 | return true; 51 | } 52 | 53 | @Override 54 | public boolean isEnabled() { 55 | return this.getEnable() != null && this.getEnable(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /resource-server/src/main/resources/generator/generatorConfig.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 |
30 | 31 | 32 |
33 |
34 |
-------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/generator/model/Client.java: -------------------------------------------------------------------------------- 1 | package com.example.generator.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.Id; 9 | import javax.persistence.Table; 10 | 11 | /** 12 | * 表名:t_client 13 | * 表注释:client表 14 | */ 15 | @Getter 16 | @Setter 17 | @Table(name = "t_client") 18 | public class Client { 19 | @Id 20 | @Column(name = "id") 21 | @GeneratedValue(generator = "JDBC") 22 | private Integer id; 23 | 24 | @Column(name = "client_id") 25 | private String clientId; 26 | 27 | @Column(name = "client_secret") 28 | private String clientSecret; 29 | 30 | /** 31 | * access_token 有效期,单位:秒,默认7200秒,即2小时。 32 | */ 33 | @Column(name = "access_token_validity_seconds") 34 | private Integer accessTokenValiditySeconds; 35 | 36 | /** 37 | * refresh_token 有效期,单位:秒。默认172800秒,即2天。 38 | */ 39 | @Column(name = "refresh_token_validity_seconds") 40 | private Integer refreshTokenValiditySeconds; 41 | 42 | /** 43 | * 授权类型 44 | */ 45 | @Column(name = "authorized_grant_types_str") 46 | private String authorizedGrantTypesStr; 47 | 48 | /** 49 | * 跳转 uri 50 | */ 51 | @Column(name = "registered_redirect_uri_str") 52 | private String registeredRedirectUriStr; 53 | 54 | @Column(name = "scope_str") 55 | private String scopeStr; 56 | 57 | @Column(name = "auto_approve_scope") 58 | private String autoApproveScope; 59 | 60 | @Column(name = "authorities_str") 61 | private String authoritiesStr; 62 | 63 | @Column(name = "additional_information_str") 64 | private String additionalInformationStr; 65 | 66 | @Column(name = "resource_ids_str") 67 | private String resourceIdsStr; 68 | } -------------------------------------------------------------------------------- /demo-frontend/src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {HttpClientModule} from "@angular/common/http"; 4 | import {FormsModule, ReactiveFormsModule} from "@angular/forms"; 5 | import {NzMenuModule} from "ng-zorro-antd/menu"; 6 | import {NzFormModule} from "ng-zorro-antd/form"; 7 | import {NzLayoutModule} from "ng-zorro-antd/layout"; 8 | import {IconsProviderModule} from "./icons-provider.module"; 9 | import {NzDividerModule} from "ng-zorro-antd/divider"; 10 | import {NzButtonModule} from "ng-zorro-antd/button"; 11 | import {NzTableModule} from "ng-zorro-antd/table"; 12 | import {NzInputModule} from "ng-zorro-antd/input"; 13 | import {NzSelectModule} from "ng-zorro-antd/select"; 14 | import {NzMessageModule} from "ng-zorro-antd/message"; 15 | import {GenderPipe} from "./pipe/gender.pipe"; 16 | import {NzCheckboxModule} from "ng-zorro-antd/checkbox"; 17 | 18 | const THIRD_MODULES: any[] = [ 19 | FormsModule, 20 | ReactiveFormsModule, 21 | HttpClientModule, 22 | ]; 23 | const NG_ZORRO_MODULES: any[] = [ 24 | IconsProviderModule, 25 | NzLayoutModule, 26 | NzFormModule, 27 | NzMenuModule, 28 | NzButtonModule, 29 | NzTableModule, 30 | NzDividerModule, 31 | NzInputModule, 32 | NzSelectModule, 33 | NzMessageModule, 34 | NzCheckboxModule, 35 | ]; 36 | const COMPONENTS: any[] = []; 37 | const DIRECTIVES: any[] = []; 38 | const PIPES: any[] = [ 39 | GenderPipe, 40 | ]; 41 | 42 | @NgModule({ 43 | declarations: [ 44 | ...PIPES, 45 | ...COMPONENTS, 46 | ...DIRECTIVES, 47 | ], 48 | imports: [ 49 | CommonModule, 50 | ...THIRD_MODULES, 51 | ...NG_ZORRO_MODULES, 52 | ], 53 | exports: [ 54 | ...THIRD_MODULES, 55 | ...NG_ZORRO_MODULES, 56 | ...PIPES, 57 | ...COMPONENTS, 58 | ...DIRECTIVES, 59 | ], 60 | }) 61 | export class SharedModule { 62 | } 63 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/register/register.component.html: -------------------------------------------------------------------------------- 1 |

注册用户

2 | 3 |
4 | 5 | 用户名 6 | 7 | 8 | 9 | 10 | 11 | 密码 12 | 13 | 15 | 16 | 17 | 18 | 确认密码 19 | 20 | 21 | 22 | 请输入确认密码! 23 | 两次密码不相同! 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | -------------------------------------------------------------------------------- /demo-frontend/src/app/shared/guard/login.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivate, 5 | CanActivateChild, 6 | Router, 7 | RouterStateSnapshot, 8 | UrlTree 9 | } from '@angular/router'; 10 | import {map, Observable} from 'rxjs'; 11 | import {HttpClient} from "@angular/common/http"; 12 | import {CommonService} from "../common/common.service"; 13 | 14 | @Injectable({ 15 | providedIn: 'root' 16 | }) 17 | export class LoginGuard implements CanActivate, CanActivateChild { 18 | constructor( 19 | private http: HttpClient, 20 | private router: Router, 21 | private service: CommonService, 22 | ) { 23 | } 24 | 25 | getTokenByCode(code: string, state: string | null): Observable { 26 | const grantType = 'authorization_code' 27 | const param = `grant_type=${grantType}&code=${code}&redirect_uri=http://localhost:4200/login` 28 | const url = `/oauth/token?${param}`; 29 | const headers = { 30 | 'Authorization': `Basic ${btoa('client:secret')}` 31 | } 32 | return this.http.post(url, null, {headers}).pipe(map(r => { 33 | this.service.token = r 34 | this.service.success(`登录成功`) 35 | if (!!state) { 36 | this.router.navigate([state]) 37 | } else { 38 | this.router.navigate(['/index']) 39 | } 40 | return false 41 | })) 42 | } 43 | 44 | canActivate( 45 | route: ActivatedRouteSnapshot, 46 | state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { 47 | const code = route.queryParamMap.get('code') 48 | if (!!code) { 49 | const state = route.queryParamMap.get('state') 50 | return this.getTokenByCode(code, state) 51 | } 52 | return true; 53 | } 54 | 55 | canActivateChild( 56 | childRoute: ActivatedRouteSnapshot, 57 | state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { 58 | console.log('canActivateChild') 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /resource-server/src/main/java/com/example/user/UserService.java: -------------------------------------------------------------------------------- 1 | package com.example.user; 2 | 3 | import com.example.generator.mapper.UserMapper; 4 | import com.example.generator.model.User; 5 | import com.example.user.vo.ResetUserPasswordVO; 6 | import com.example.user.vo.UserDetailVO; 7 | import com.example.user.vo.UserVO; 8 | import org.springframework.security.crypto.password.PasswordEncoder; 9 | import org.springframework.stereotype.Service; 10 | import tk.mybatis.mapper.entity.Example; 11 | 12 | import javax.annotation.Resource; 13 | import java.util.List; 14 | 15 | @Service 16 | public class UserService { 17 | @Resource 18 | UserMapper userMapper; 19 | @Resource 20 | PasswordEncoder passwordEncoder; 21 | 22 | public List userList() { 23 | return userMapper.selectAllFetchDetail(null); 24 | } 25 | 26 | public UserVO showUserDetail(Integer id) { 27 | return userMapper.selectAllFetchDetail(id).get(0); 28 | } 29 | 30 | public void resetUserPassword(ResetUserPasswordVO resetUserPasswordVO) { 31 | User user = getUserByUsername(resetUserPasswordVO.getUsername()); 32 | user.setPassword(passwordEncoder.encode(resetUserPasswordVO.getPassword())); 33 | userMapper.updateByPrimaryKeySelective(user); 34 | } 35 | 36 | public User getUserByUsername(String username) { 37 | Example example = new Example(User.class); 38 | example.createCriteria().andEqualTo("username", username); 39 | return userMapper.selectOneByExample(example); 40 | } 41 | 42 | public List showPersonalRole(String username) { 43 | return userMapper.showPersonalRole(username); 44 | } 45 | 46 | public UserDetailVO showPersonalDetail(String username) { 47 | return userMapper.showPersonalDetail(username); 48 | } 49 | 50 | public void updatePersonalDetail(UserDetailVO userDetailVO, String username) { 51 | User user = getUserByUsername(username); 52 | userDetailVO.setUserId(user.getId()); 53 | userMapper.updatePersonalDetail(userDetailVO); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/reset-user-password/reset-user-password.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {ActivatedRoute, Router} from "@angular/router"; 3 | import {CommonService} from "../../shared/common/common.service"; 4 | import {HttpClient} from "@angular/common/http"; 5 | import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms"; 6 | 7 | @Component({ 8 | selector: 'app-reset-user-password', 9 | templateUrl: './reset-user-password.component.html', 10 | styleUrls: ['./reset-user-password.component.scss'] 11 | }) 12 | export class ResetUserPasswordComponent implements OnInit { 13 | validateForm: FormGroup; 14 | 15 | constructor( 16 | private route: ActivatedRoute, 17 | private router: Router, 18 | private service: CommonService, 19 | private http: HttpClient, 20 | private fb: FormBuilder, 21 | ) { 22 | this.validateForm = this.fb.group({ 23 | username: [{value: null, disabled: true}], 24 | password: [null, [Validators.required]], 25 | passwordAgain: [null, [Validators.required, this.confirmationValidator]], 26 | }) 27 | } 28 | 29 | ngOnInit(): void { 30 | this.route.queryParams.subscribe(r => { 31 | if (!r['id']) { 32 | this.service.info(`请先选择用户`) 33 | this.router.navigate(['/showUserList']) 34 | } else { 35 | this.http.get(`/user/details/${r['id']}`).subscribe(r1 => { 36 | this.validateForm.patchValue({username: r1['username']}) 37 | }) 38 | } 39 | }) 40 | } 41 | 42 | get password() { 43 | return this.validateForm.get('password') as FormControl 44 | } 45 | 46 | confirmationValidator = (control: FormControl): { [s: string]: boolean } => { 47 | if (!control.value) { 48 | return {required: true}; 49 | } else if (control.value !== this.password.value) { 50 | return {confirm: true, error: true}; 51 | } 52 | return {}; 53 | }; 54 | 55 | submitForm() { 56 | this.http.post(`/user/resetUserPassword`, this.validateForm.getRawValue()).subscribe(() => { 57 | this.service.success(`密码重置成功,即将跳转到用户列表`) 58 | this.router.navigate(['/showUserList']) 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /demo-frontend/src/app/pages/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms"; 3 | import {HttpClient} from "@angular/common/http"; 4 | import {Router} from "@angular/router"; 5 | import {CommonService} from "../../shared/common/common.service"; 6 | 7 | @Component({ 8 | selector: 'app-register', 9 | templateUrl: './register.component.html', 10 | styleUrls: ['./register.component.scss'] 11 | }) 12 | export class RegisterComponent implements OnInit { 13 | validateForm: FormGroup; 14 | 15 | constructor( 16 | private fb: FormBuilder, 17 | private http: HttpClient, 18 | private router: Router, 19 | private service: CommonService, 20 | ) { 21 | this.validateForm = this.fb.group({ 22 | username: [null, [Validators.required]], 23 | password: [null, [Validators.required]], 24 | passwordAgain: [null, [Validators.required, this.confirmationValidator]], 25 | agree: [false], 26 | }) 27 | } 28 | 29 | ngOnInit(): void { 30 | } 31 | 32 | get password() { 33 | return this.validateForm.get('password') as FormControl 34 | } 35 | 36 | confirmationValidator = (control: FormControl): { [s: string]: boolean } => { 37 | if (!control.value) { 38 | return {required: true}; 39 | } else if (control.value !== this.password.value) { 40 | return {confirm: true, error: true}; 41 | } 42 | return {}; 43 | }; 44 | 45 | submitForm() { 46 | if (this.validateForm.get('agree')?.value === false) { 47 | this.service.info(`请阅读并同意协议`) 48 | return 49 | } 50 | this.http.post(`/oauth/register`, this.validateForm.getRawValue()).subscribe({ 51 | next: () => { 52 | this.service.success(`注册成功,即将跳转到登录页面`) 53 | this.router.navigate(['/login']) 54 | }, 55 | error: (e) => { 56 | if (e?.status === 400 && e?.error?.errors[0]?.defaultMessage) { 57 | this.service.error(`注册失败:${e.error.errors[0].defaultMessage}`) 58 | } else { 59 | this.service.error(`注册失败`) 60 | } 61 | } 62 | }) 63 | } 64 | 65 | agreement() { 66 | this.service.info(`哎,啥都没有`) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /authorization-server/src/test/java/com/example/utils/TestRequestUtils.java: -------------------------------------------------------------------------------- 1 | package com.example.utils; 2 | 3 | import cn.hutool.json.JSONObject; 4 | import cn.hutool.json.JSONUtil; 5 | import lombok.Data; 6 | import org.springframework.http.HttpEntity; 7 | import org.springframework.http.HttpHeaders; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.util.LinkedMultiValueMap; 10 | import org.springframework.util.MultiValueMap; 11 | 12 | /** 13 | * 测试时构建请求参数 14 | */ 15 | public class TestRequestUtils { 16 | public static JsonRequestBuilder jsonRequest() { 17 | JsonRequestBuilder builder = new JsonRequestBuilder(); 18 | builder.getHeaders().setContentType(MediaType.APPLICATION_JSON); 19 | return builder; 20 | } 21 | 22 | public static UrlEncodedRequestBuilder urlEncodedRequest() { 23 | UrlEncodedRequestBuilder builder = new UrlEncodedRequestBuilder(); 24 | builder.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED); 25 | return builder; 26 | } 27 | 28 | @Data 29 | public static class UrlEncodedRequestBuilder { 30 | MultiValueMap body = new LinkedMultiValueMap<>(); 31 | HttpHeaders headers = new HttpHeaders(); 32 | 33 | public UrlEncodedRequestBuilder put(String key, String value) { 34 | this.getBody().add(key, value); 35 | return this; 36 | } 37 | 38 | public UrlEncodedRequestBuilder header(String name, String value) { 39 | this.getHeaders().set(name, value); 40 | return this; 41 | } 42 | 43 | public HttpEntity> build() { 44 | return new HttpEntity<>(body, headers); 45 | } 46 | } 47 | 48 | @Data 49 | public static class JsonRequestBuilder { 50 | HttpHeaders headers = new HttpHeaders(); 51 | JSONObject body = JSONUtil.createObj(); 52 | 53 | public JsonRequestBuilder set(String key, String value) { 54 | this.getBody().set(key, value); 55 | return this; 56 | } 57 | 58 | public JsonRequestBuilder header(String name, String value) { 59 | this.getHeaders().set(name, value); 60 | return this; 61 | } 62 | 63 | public HttpEntity build() { 64 | return new HttpEntity<>(body, headers); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /resource-server/src/main/java/com/example/user/UserController.java: -------------------------------------------------------------------------------- 1 | package com.example.user; 2 | 3 | import com.example.user.vo.ResetUserPasswordVO; 4 | import com.example.user.vo.UserDetailVO; 5 | import com.example.user.vo.UserVO; 6 | import org.springframework.security.access.prepost.PreAuthorize; 7 | import org.springframework.validation.annotation.Validated; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import javax.annotation.Resource; 11 | import java.security.Principal; 12 | import java.util.List; 13 | 14 | @RestController 15 | public class UserController { 16 | @Resource 17 | UserService userService; 18 | 19 | /** 20 | * 查看用户列表 21 | */ 22 | @GetMapping("/user") 23 | @PreAuthorize("hasAuthority('showUserList')") 24 | public List userList() { 25 | return userService.userList(); 26 | } 27 | 28 | /** 29 | * 查看用户详情 30 | */ 31 | @GetMapping("/user/details/{id}") 32 | @PreAuthorize("hasAuthority('showUserDetail')") 33 | public UserVO showUserDetail(@PathVariable Integer id) { 34 | return userService.showUserDetail(id); 35 | } 36 | 37 | /** 38 | * 重置用户密码 39 | */ 40 | @PostMapping("/user/resetUserPassword") 41 | @PreAuthorize("hasAuthority('resetUserPassword')") 42 | public void resetUserPassword(@RequestBody @Validated ResetUserPasswordVO resetUserPasswordVO) { 43 | userService.resetUserPassword(resetUserPasswordVO); 44 | } 45 | 46 | /** 47 | * 查看个人角色 48 | */ 49 | @GetMapping("/user/showPersonalRole") 50 | @PreAuthorize("hasAuthority('showPersonalRole')") 51 | public List showPersonalRole(Principal principal) { 52 | String username = principal.getName(); 53 | return userService.showPersonalRole(username); 54 | } 55 | 56 | /** 57 | * 查看个人信息 58 | */ 59 | @GetMapping("/user/showPersonalDetail") 60 | @PreAuthorize("hasAuthority('showPersonalDetail')") 61 | public UserDetailVO showPersonalDetail(Principal principal) { 62 | String username = principal.getName(); 63 | return userService.showPersonalDetail(username); 64 | } 65 | 66 | /** 67 | * 编辑个人信息 68 | */ 69 | @PostMapping("/user/updatePersonalDetail") 70 | @PreAuthorize("hasAuthority('updatePersonalDetail')") 71 | public void updatePersonalDetail(@RequestBody UserDetailVO userDetailVO, Principal principal) { 72 | String username = principal.getName(); 73 | userService.updatePersonalDetail(userDetailVO, username); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /demo-frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /authorization-server/src/main/resources/mapper/UserMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 22 | 23 | 45 | 46 | insert into r_user_role (user_id, role_id) 47 | values (#{0}, 1) 48 | 49 | 50 | update t_user 51 | set password = #{param2} 52 | where username = #{param1} 53 | 54 | -------------------------------------------------------------------------------- /demo-frontend/src/app/shared/common/common.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {map, Observable, Subject} from "rxjs"; 3 | import {NzMessageService} from "ng-zorro-antd/message"; 4 | import {HttpClient} from "@angular/common/http"; 5 | import {Router} from "@angular/router"; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class CommonService { 11 | private tokenSource = new Subject() 12 | private userSource = new Subject() 13 | token$ = this.tokenSource.asObservable() 14 | user$ = this.userSource.asObservable() 15 | 16 | constructor( 17 | private message: NzMessageService, 18 | private http: HttpClient, 19 | private router: Router, 20 | ) { 21 | } 22 | 23 | get user() { 24 | return JSON.parse(localStorage.getItem('user') || '{}') 25 | } 26 | 27 | set user(user: any) { 28 | localStorage.setItem('user', JSON.stringify(user)) 29 | this.userSource.next(user) 30 | } 31 | 32 | get expired() { 33 | // 提前10秒,避免踩点过期 34 | return this.user.exp * 1000 - 10000 < new Date().getTime() 35 | } 36 | 37 | clearAuth() { 38 | this.user = null 39 | this.token = null 40 | } 41 | 42 | noAuth() { 43 | this.clearAuth() 44 | this.router.navigate(['/index']) 45 | } 46 | 47 | get token() { 48 | return JSON.parse(localStorage.getItem('token') || '{}') 49 | } 50 | 51 | set token(token: any) { 52 | if (!!token) { 53 | const user = atob(token['access_token'].split('.')[1]) 54 | this.user = JSON.parse(user) 55 | } 56 | localStorage.setItem('token', JSON.stringify(token)) 57 | this.tokenSource.next(token) 58 | } 59 | 60 | success(message = `保存成功,正在返回列表`) { 61 | this.message.success(message) 62 | } 63 | 64 | error(message = `保存失败,请联系管理员`) { 65 | this.message.error(message) 66 | } 67 | 68 | info(message = `警告`) { 69 | this.message.info(message) 70 | } 71 | 72 | refreshToken(): Observable { 73 | let param = `grant_type=refresh_token&refresh_token=${this.token.refresh_token}` 74 | let url = `/oauth/token?${param}` 75 | let headers = { 76 | 'Authorization': `Basic ${btoa('client:secret')}` 77 | } 78 | return this.http.post(url, null, {headers}).pipe(map(r => { 79 | this.token = r 80 | return true 81 | })) 82 | } 83 | 84 | login(username: string, password: string) { 85 | let param = `grant_type=password&username=${username}&password=${password}` 86 | let url = `/oauth/token?${param}` 87 | let headers = { 88 | 'Authorization': `Basic ${btoa('client:secret')}` 89 | } 90 | return this.http.post(url, null, {headers}) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /resource-server/src/test/java/com/example/user/UserServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.example.user; 2 | 3 | import com.example.generator.mapper.UserMapper; 4 | import com.example.generator.model.User; 5 | import com.example.user.vo.ResetUserPasswordVO; 6 | import com.example.user.vo.UserVO; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.Mockito; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 15 | import org.springframework.security.crypto.password.PasswordEncoder; 16 | import tk.mybatis.mapper.entity.Config; 17 | import tk.mybatis.mapper.mapperhelper.EntityHelper; 18 | 19 | import java.util.Arrays; 20 | import java.util.List; 21 | 22 | import static org.junit.jupiter.api.Assertions.assertEquals; 23 | import static org.mockito.ArgumentMatchers.any; 24 | 25 | @ExtendWith(MockitoExtension.class) 26 | class UserServiceTest { 27 | @InjectMocks 28 | UserService userService; 29 | @Mock 30 | UserMapper userMapper; 31 | @Mock 32 | PasswordEncoder passwordEncoder; 33 | 34 | @BeforeEach 35 | void setUp() { 36 | // mapper插件在测试之前要初始化table,否则构建example会失败 37 | EntityHelper.initEntityNameMap(User.class, new Config()); 38 | } 39 | 40 | @Test 41 | void userList() { 42 | Mockito.when(userMapper.selectAllFetchDetail(null)).thenReturn(Arrays.asList(new UserVO(), new UserVO())); 43 | List userVOS = userService.userList(); 44 | assertEquals(2, userVOS.size()); 45 | } 46 | 47 | @Test 48 | void showUserDetail() { 49 | UserVO userVO = new UserVO(); 50 | Mockito.when(userMapper.selectAllFetchDetail(1)).thenReturn(List.of(userVO)); 51 | UserVO result = userService.showUserDetail(1); 52 | assertEquals(result, userVO); 53 | } 54 | 55 | @Test 56 | void resetUserPassword() { 57 | String username = "username"; 58 | String password = "password"; 59 | 60 | String encode = new BCryptPasswordEncoder().encode(password); 61 | Mockito.when(passwordEncoder.encode(password)).thenReturn(encode); 62 | 63 | User user = new User(); 64 | user.setUsername(username); 65 | user.setPassword(encode); 66 | 67 | Mockito.when(userMapper.selectOneByExample(any())).thenReturn(user); 68 | 69 | ResetUserPasswordVO vo = new ResetUserPasswordVO(); 70 | vo.setUsername(username); 71 | vo.setPassword(password); 72 | userService.resetUserPassword(vo); 73 | } 74 | } -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/config/security/CustomLogoutHandler.java: -------------------------------------------------------------------------------- 1 | package com.example.config.security; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.oauth2.common.OAuth2AccessToken; 5 | import org.springframework.security.oauth2.provider.token.TokenStore; 6 | import org.springframework.security.web.authentication.logout.LogoutHandler; 7 | import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.util.ObjectUtils; 10 | 11 | import javax.annotation.Resource; 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.io.IOException; 15 | 16 | /** 17 | * 自定义注销相关处理逻辑 18 | */ 19 | @Component 20 | public class CustomLogoutHandler implements LogoutHandler, LogoutSuccessHandler { 21 | @Resource 22 | TokenStore tokenStore; 23 | 24 | /** 25 | * 注销逻辑 26 | */ 27 | @Override 28 | public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { 29 | // 从请求中获取token,这一步也可以根据自己实际使用场景来获取,比如在header中取等等 30 | String token; 31 | if (!ObjectUtils.isEmpty(request.getHeader("Authorization")) && request.getHeader("Authorization").contains("bearer ")) { 32 | token = request.getHeader("Authorization").split("bearer ")[1]; 33 | } else if (!ObjectUtils.isEmpty(request.getParameter("token"))) { 34 | token = request.getParameter("token"); 35 | } else if (!ObjectUtils.isEmpty(request.getParameter("access_token"))) { 36 | token = request.getParameter("access_token"); 37 | } else { 38 | return; 39 | } 40 | // 在token存储器中查询token 41 | OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token); 42 | if (oAuth2AccessToken == null) { 43 | return; 44 | } 45 | if (oAuth2AccessToken.getRefreshToken() != null) { 46 | // 在token存储中移除refresh_token 47 | tokenStore.removeRefreshToken(oAuth2AccessToken.getRefreshToken()); 48 | } 49 | // 在token存储中移除access_token 50 | tokenStore.removeAccessToken(oAuth2AccessToken); 51 | } 52 | 53 | /** 54 | * 注销成功后逻辑 55 | */ 56 | @Override 57 | public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { 58 | // 在请求中获取重定向地址,这一步也可以根据自己实际使用场景来获取,比如在header中取等等 59 | String redirect = request.getParameter("redirect"); 60 | // 重定向到新地址 61 | response.sendRedirect(redirect); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/client/ClientVO.java: -------------------------------------------------------------------------------- 1 | package com.example.client; 2 | 3 | import cn.hutool.core.bean.BeanUtil; 4 | import cn.hutool.json.JSONUtil; 5 | import com.example.generator.model.Client; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import org.springframework.security.core.GrantedAuthority; 9 | import org.springframework.security.oauth2.provider.ClientDetails; 10 | import org.springframework.util.ObjectUtils; 11 | import org.springframework.util.StringUtils; 12 | 13 | import java.util.Collection; 14 | import java.util.Map; 15 | import java.util.Set; 16 | import java.util.stream.Collectors; 17 | 18 | @EqualsAndHashCode(callSuper = true) 19 | @Data 20 | public class ClientVO extends Client implements ClientDetails { 21 | public static ClientVO from(Client client) { 22 | ClientVO clientVO = new ClientVO(); 23 | BeanUtil.copyProperties(client, clientVO); 24 | return clientVO; 25 | } 26 | 27 | @Override 28 | public Set getResourceIds() { 29 | return StringUtils.commaDelimitedListToSet(this.getResourceIdsStr()); 30 | } 31 | 32 | @Override 33 | public boolean isSecretRequired() { 34 | return !ObjectUtils.isEmpty(this.getClientSecret()); 35 | } 36 | 37 | @Override 38 | public boolean isScoped() { 39 | return !ObjectUtils.isEmpty(this.getScope()); 40 | } 41 | 42 | @Override 43 | public Set getScope() { 44 | return StringUtils.commaDelimitedListToSet(this.getScopeStr()); 45 | } 46 | 47 | @Override 48 | public Set getAuthorizedGrantTypes() { 49 | return StringUtils.commaDelimitedListToSet(this.getAuthorizedGrantTypesStr()); 50 | } 51 | 52 | @Override 53 | public Set getRegisteredRedirectUri() { 54 | return StringUtils.commaDelimitedListToSet(this.getRegisteredRedirectUriStr()); 55 | } 56 | 57 | @Override 58 | public Collection getAuthorities() { 59 | return StringUtils.commaDelimitedListToSet(this.getAuthoritiesStr()) 60 | .stream() 61 | .map(s -> (GrantedAuthority) () -> s) 62 | .collect(Collectors.toList()); 63 | } 64 | 65 | @Override 66 | public boolean isAutoApprove(String scope) { 67 | return !ObjectUtils.isEmpty(this.getAutoApproveScope()) 68 | && StringUtils.commaDelimitedListToSet(this.getAutoApproveScope()).containsAll(this.getScope()); 69 | } 70 | 71 | @Override 72 | public Map getAdditionalInformation() { 73 | if (ObjectUtils.isEmpty(this.getAdditionalInformationStr())) { 74 | return null; 75 | } 76 | return JSONUtil.parseObj(this.getAdditionalInformationStr()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /authorization-server/src/test/resources/sql/unit-test-schema.sql: -------------------------------------------------------------------------------- 1 | create schema `spring-boot-oauth2-example`; 2 | 3 | create table `t_client` 4 | ( 5 | id int auto_increment primary key, 6 | client_id varchar(100) not null, 7 | client_secret varchar(100) not null, 8 | access_token_validity_seconds int default 7200 not null comment 'access_token 有效期,单位:秒,默认7200秒,即2小时。', 9 | refresh_token_validity_seconds int default 172800 null comment 'refresh_token 有效期,单位:秒。默认172800秒,即2天。', 10 | authorized_grant_types_str varchar(200) default 'authorization_code,refresh_token,password' not null comment '授权类型', 11 | registered_redirect_uri_str varchar(500) null comment '跳转 uri', 12 | scope_str varchar(200) default 'app' not null, 13 | auto_approve_scope varchar(200) default 'app' null, 14 | authorities_str varchar(200) null, 15 | additional_information_str varchar(200) null, 16 | resource_ids_str varchar(200) null, 17 | constraint t_client_client_id_uindex unique (client_id) 18 | ); 19 | 20 | create table t_user 21 | ( 22 | id int auto_increment primary key, 23 | username varchar(100) not null comment '用户名', 24 | password varchar(100) not null comment '密码,根据需求加密后存储', 25 | enable bit default 1 not null comment '是否可用,1为可用,0为不可用,默认1可用', 26 | constraint t_user_username_uindex unique (username) 27 | ); 28 | 29 | create table t_role 30 | ( 31 | id int auto_increment primary key, 32 | name varchar(200) not null comment '角色名', 33 | description varchar(200) not null comment '角色描述', 34 | constraint t_role_name_uindex unique (name) 35 | ); 36 | 37 | create table t_resource 38 | ( 39 | id int auto_increment primary key, 40 | name varchar(40) not null comment '资源名,唯一,英文,尽量短', 41 | description varchar(200) not null comment '资源描述', 42 | constraint t_resource_name_uindex unique (name) 43 | ); 44 | 45 | create table r_role_resource 46 | ( 47 | role_id int not null, 48 | resource_id int not null, 49 | primary key (role_id, resource_id) 50 | ); 51 | 52 | create table r_user_role 53 | ( 54 | user_id int not null, 55 | role_id int not null, 56 | primary key (user_id, role_id) 57 | ); 58 | 59 | -------------------------------------------------------------------------------- /resource-server/src/test/java/com/example/user/UserProcessByMySQLModeTest.java: -------------------------------------------------------------------------------- 1 | package com.example.user; 2 | 3 | import cn.hutool.json.JSONUtil; 4 | import com.example.config.TestResourceConfig; 5 | import com.example.user.vo.UserDetailVO; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.context.annotation.Import; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.security.test.context.support.WithMockUser; 12 | import org.springframework.test.context.ActiveProfiles; 13 | import org.springframework.test.web.servlet.MockMvc; 14 | 15 | import javax.annotation.Resource; 16 | 17 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 18 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 19 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 20 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 21 | 22 | /** 23 | * 由于sql中使用了 on duplicate key update 语句, 24 | * h2数据库不支持,需要在配置中加上 MODE=MySQL; 25 | *
26 | * 而加上之后可能会影响其它测试用例,所以就提取到单独的一个测试类中 27 | */ 28 | @SpringBootTest 29 | @AutoConfigureMockMvc 30 | @ActiveProfiles({"unit-test", "unit-test-mysql-mode"}) 31 | @Import(TestResourceConfig.class) 32 | public class UserProcessByMySQLModeTest { 33 | @Resource 34 | MockMvc mockMvc; 35 | 36 | /** 37 | * {@link UserController#updatePersonalDetail(UserDetailVO)} 38 | */ 39 | @Test 40 | @WithMockUser(authorities = {"updatePersonalDetail", "showPersonalDetail"}) 41 | void updatePersonalDetail() throws Exception { 42 | String name = "测试用户111"; 43 | Integer age = 22; 44 | String email = "test111@test.com"; 45 | Integer gender = 2; 46 | UserDetailVO vo = new UserDetailVO(); 47 | vo.setUserId(1); 48 | vo.setName(name); 49 | vo.setAge(age); 50 | vo.setEmail(email); 51 | vo.setGender(gender); 52 | mockMvc.perform(post("/user/updatePersonalDetail") 53 | .contentType(MediaType.APPLICATION_JSON) 54 | .content(JSONUtil.toJsonStr(vo)) 55 | ).andExpect(status().isOk()) 56 | ; 57 | mockMvc.perform(get("/user/showPersonalDetail")) 58 | .andExpect(status().isOk()) 59 | .andExpect(jsonPath("$.id").value(1)) 60 | .andExpect(jsonPath("$.userId").value(1)) 61 | .andExpect(jsonPath("$.name").value(name)) 62 | .andExpect(jsonPath("$.age").value(age)) 63 | .andExpect(jsonPath("$.email").value(email)) 64 | .andExpect(jsonPath("$.gender").value(gender)) 65 | ; 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/config/security/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.config.security; 2 | 3 | import com.example.user.UserService; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.authentication.AuthenticationManager; 7 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 8 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 9 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 12 | import org.springframework.security.crypto.password.PasswordEncoder; 13 | 14 | import javax.annotation.Resource; 15 | 16 | /** 17 | * Web安全相关配置 18 | */ 19 | @Configuration 20 | @EnableWebSecurity 21 | @EnableGlobalMethodSecurity(prePostEnabled = true) 22 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 23 | @Resource 24 | UserService userService; 25 | @Resource 26 | PasswordEncoder passwordEncoder; 27 | @Resource 28 | CustomLogoutHandler customLogoutHandler; 29 | 30 | /** 31 | * 为密码方式提供认证管理器 32 | */ 33 | @Bean 34 | @Override 35 | public AuthenticationManager authenticationManagerBean() throws Exception { 36 | return super.authenticationManagerBean(); 37 | } 38 | 39 | /** 40 | * 构建认证管理器 41 | */ 42 | @Override 43 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 44 | auth 45 | // 提供用户详情实现 46 | .userDetailsService(userService) 47 | // 指定加密方式 48 | .passwordEncoder(passwordEncoder); 49 | } 50 | 51 | /** 52 | * http安全配置 53 | */ 54 | @Override 55 | protected void configure(HttpSecurity http) throws Exception { 56 | http.authorizeRequests() 57 | // 注册接口允许未登录用户访问 58 | .antMatchers("/oauth/register").anonymous() 59 | // 修改密码接口允许所有人访问 60 | .antMatchers("/oauth/changePassword").permitAll() 61 | // 其余未指定的所有请求都需要授权 62 | .anyRequest().authenticated(); 63 | http 64 | // 注销相关逻辑配置 65 | .logout() 66 | // 注销接口的url 67 | .logoutUrl("/oauth/logout") 68 | // 添加注销处理逻辑 69 | .addLogoutHandler(customLogoutHandler) 70 | // 注销之后的逻辑 71 | .logoutSuccessHandler(customLogoutHandler); 72 | // 关闭csrf校验 73 | http.csrf().disable(); 74 | // 开启登录页面支持 75 | http.formLogin(); 76 | // 开启http basic认证方式 77 | http.httpBasic(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /demo-frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 14 | 43 | 44 | 45 | 46 |
47 | 48 | 52 | 53 | 54 | 59 | 60 | 64 | 65 |
66 |
67 | 68 |
69 | 70 |
71 |
72 |
73 |
74 | -------------------------------------------------------------------------------- /resource-server/src/main/resources/mapper/UserMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 34 | 46 | 48 | 49 | 50 | 62 | 71 | -------------------------------------------------------------------------------- /demo-frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {BrowserModule} from '@angular/platform-browser'; 3 | 4 | import {AppComponent} from './app.component'; 5 | import {RouterModule, Routes} from "@angular/router"; 6 | import {NZ_I18N, zh_CN} from 'ng-zorro-antd/i18n'; 7 | import {registerLocaleData} from '@angular/common'; 8 | import zh from '@angular/common/locales/zh'; 9 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 10 | import {SharedModule} from "./shared/shared.module"; 11 | import {IndexComponent} from './pages/index/index.component'; 12 | import {LoginGuard} from "./shared/guard/login.guard"; 13 | import {ShowPersonalRoleComponent} from './pages/show-personal-role/show-personal-role.component'; 14 | import {UpdatePersonalDetailComponent} from './pages/update-personal-detail/update-personal-detail.component'; 15 | import {ShowUserListComponent} from './pages/show-user-list/show-user-list.component'; 16 | import {ShowUserDetailComponent} from './pages/show-user-detail/show-user-detail.component'; 17 | import {ResetUserPasswordComponent} from './pages/reset-user-password/reset-user-password.component'; 18 | import {ShowPersonalDetailComponent} from './pages/show-personal-detail/show-personal-detail.component'; 19 | import {AuthGuard} from "./shared/guard/auth.guard"; 20 | import {LoginComponent} from './pages/login/login.component'; 21 | import {HTTP_INTERCEPTORS} from "@angular/common/http"; 22 | import {AuthInterceptor} from "./shared/common/auth.interceptor"; 23 | import {RegisterComponent} from './pages/register/register.component'; 24 | 25 | registerLocaleData(zh); 26 | 27 | const routes: Routes = [ 28 | {path: '', redirectTo: 'index', pathMatch: 'full'}, 29 | {path: 'index', component: IndexComponent}, 30 | {path: 'login', component: LoginComponent, canActivate: [LoginGuard]}, 31 | { 32 | path: '', 33 | canActivateChild: [AuthGuard], children: [ 34 | {path: 'showPersonalRole', component: ShowPersonalRoleComponent}, 35 | {path: 'updatePersonalDetail', component: UpdatePersonalDetailComponent}, 36 | {path: 'showUserList', component: ShowUserListComponent}, 37 | {path: 'showUserDetail', component: ShowUserDetailComponent}, 38 | {path: 'resetUserPassword', component: ResetUserPasswordComponent}, 39 | {path: 'showPersonalDetail', component: ShowPersonalDetailComponent}, 40 | ] 41 | }, 42 | {path: 'register', component: RegisterComponent}, 43 | ] 44 | 45 | @NgModule({ 46 | declarations: [ 47 | AppComponent, 48 | IndexComponent, 49 | ShowPersonalRoleComponent, 50 | UpdatePersonalDetailComponent, 51 | ShowUserListComponent, 52 | ShowUserDetailComponent, 53 | ResetUserPasswordComponent, 54 | ShowPersonalDetailComponent, 55 | LoginComponent, 56 | RegisterComponent, 57 | ], 58 | imports: [ 59 | BrowserModule, 60 | BrowserAnimationsModule, 61 | RouterModule.forRoot(routes), 62 | SharedModule, 63 | ], 64 | providers: [ 65 | {provide: NZ_I18N, useValue: zh_CN}, 66 | {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true}, 67 | ], 68 | bootstrap: [AppComponent] 69 | }) 70 | export class AppModule { 71 | } 72 | -------------------------------------------------------------------------------- /sql/schema.sql: -------------------------------------------------------------------------------- 1 | # drop database `spring-boot-oauth2-example`; 2 | create database `spring-boot-oauth2-example`; 3 | 4 | use `spring-boot-oauth2-example`; 5 | 6 | create table `t_client` 7 | ( 8 | id int auto_increment primary key, 9 | client_id varchar(100) not null, 10 | client_secret varchar(100) not null, 11 | access_token_validity_seconds int default 7200 not null comment 'access_token 有效期,单位:秒,默认7200秒,即2小时。', 12 | refresh_token_validity_seconds int default 172800 null comment 'refresh_token 有效期,单位:秒。默认172800秒,即2天。', 13 | authorized_grant_types_str varchar(200) default 'authorization_code,refresh_token,password' not null comment '授权类型', 14 | registered_redirect_uri_str varchar(500) null comment '跳转 uri', 15 | scope_str varchar(200) default 'app' not null, 16 | auto_approve_scope varchar(200) default 'app' null, 17 | authorities_str varchar(200) null, 18 | additional_information_str varchar(200) null, 19 | resource_ids_str varchar(200) null, 20 | constraint t_client_client_id_uindex unique (client_id) 21 | ) comment 'client表'; 22 | 23 | create table t_role 24 | ( 25 | id int auto_increment primary key, 26 | name varchar(200) not null comment '角色名', 27 | description varchar(200) not null comment '角色描述', 28 | constraint t_role_name_uindex unique (name) 29 | ) comment '角色表'; 30 | 31 | create table t_resource 32 | ( 33 | id int auto_increment primary key, 34 | name varchar(40) not null comment '资源名,唯一,英文,尽量短', 35 | description varchar(200) not null comment '资源描述', 36 | constraint t_resource_name_uindex unique (name) 37 | ) comment '资源表'; 38 | 39 | create table r_role_resource 40 | ( 41 | role_id int not null, 42 | resource_id int not null, 43 | primary key (role_id, resource_id) 44 | ) comment '角色资源中间表'; 45 | 46 | create table t_user 47 | ( 48 | id int auto_increment primary key, 49 | username varchar(100) not null comment '用户名', 50 | password varchar(100) not null comment '密码,根据需求加密后存储', 51 | enable bit default 1 not null comment '是否可用,1为可用,0为不可用,默认1可用', 52 | constraint t_user_username_uindex unique (username) 53 | ) comment '用户表'; 54 | 55 | create table r_user_role 56 | ( 57 | user_id int not null, 58 | role_id int not null, 59 | primary key (user_id, role_id) 60 | ) comment '用户角色中间表'; 61 | 62 | create table t_user_detail 63 | ( 64 | id int auto_increment primary key, 65 | user_id int not null comment '关联t_user表id', 66 | name varchar(20) null comment '用户姓名', 67 | age int null comment '年龄', 68 | email varchar(100) null comment '电子邮箱', 69 | gender int null comment '性别,1为男性,2为女性,3为其它', 70 | constraint t_user_detail_user_id_uindex unique (user_id) 71 | ) comment '用户详情表'; 72 | 73 | 74 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/user/UserService.java: -------------------------------------------------------------------------------- 1 | package com.example.user; 2 | 3 | import com.example.generator.mapper.UserMapper; 4 | import com.example.generator.model.User; 5 | import com.example.user.bean.UserChangePasswordVO; 6 | import com.example.user.bean.UserRegisterVO; 7 | import com.example.user.bean.UserVO; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 11 | import org.springframework.security.crypto.password.PasswordEncoder; 12 | import org.springframework.security.oauth2.common.OAuth2AccessToken; 13 | import org.springframework.security.oauth2.provider.OAuth2Authentication; 14 | import org.springframework.security.oauth2.provider.token.TokenStore; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.util.ObjectUtils; 17 | import tk.mybatis.mapper.entity.Example; 18 | 19 | import javax.annotation.Resource; 20 | import java.util.List; 21 | 22 | import static org.hibernate.validator.internal.util.Contracts.assertNotNull; 23 | 24 | @Service 25 | public class UserService implements UserDetailsService { 26 | @Resource 27 | UserMapper userMapper; 28 | @Resource 29 | PasswordEncoder passwordEncoder; 30 | @Resource 31 | TokenStore tokenStore; 32 | 33 | public List findAllFetchRoleAndResource() { 34 | return userMapper.findAllFetchRoleAndResource(null); 35 | } 36 | 37 | @Override 38 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 39 | List allFetchRoleAndResource = userMapper.findAllFetchRoleAndResource(username); 40 | if (ObjectUtils.isEmpty(allFetchRoleAndResource)) { 41 | throw new UsernameNotFoundException("用户名不存在"); 42 | } 43 | return allFetchRoleAndResource.get(0); 44 | } 45 | 46 | public void register(UserRegisterVO userRegisterVO) { 47 | User user = new User(); 48 | user.setUsername(userRegisterVO.getUsername()); 49 | user.setPassword(passwordEncoder.encode(userRegisterVO.getPassword())); 50 | userMapper.insertSelective(user); 51 | assertNotNull(user.getId()); 52 | userMapper.initRoleByUserId(user.getId()); 53 | } 54 | 55 | public User selectUserByUsername(String username) { 56 | Example example = new Example(User.class); 57 | example.createCriteria().andEqualTo("username", username); 58 | return userMapper.selectOneByExample(example); 59 | } 60 | 61 | public void changePassword(UserChangePasswordVO userChangePasswordVO) { 62 | String token = userChangePasswordVO.getToken(); 63 | OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(token); 64 | String username = oAuth2Authentication.getName(); 65 | userMapper.changePassword(username, passwordEncoder.encode(userChangePasswordVO.getPassword())); 66 | OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(token); 67 | if (oAuth2AccessToken.getRefreshToken() != null) { 68 | // 在token存储中移除refresh_token 69 | tokenStore.removeRefreshToken(oAuth2AccessToken.getRefreshToken()); 70 | } 71 | // 在token存储中移除access_token 72 | tokenStore.removeAccessToken(oAuth2AccessToken); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/com/example/config/security/AuthConfig.java: -------------------------------------------------------------------------------- 1 | package com.example.config.security; 2 | 3 | import com.example.client.ClientService; 4 | import com.example.user.UserService; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.authentication.AuthenticationManager; 8 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 9 | import org.springframework.security.crypto.password.PasswordEncoder; 10 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 11 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 12 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 13 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 14 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; 15 | import org.springframework.security.oauth2.provider.token.AccessTokenConverter; 16 | import org.springframework.security.oauth2.provider.token.TokenStore; 17 | import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; 18 | import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; 19 | 20 | import javax.annotation.Resource; 21 | 22 | /** 23 | * 授权服务器相关配置 24 | */ 25 | @Configuration 26 | @EnableAuthorizationServer 27 | public class AuthConfig extends AuthorizationServerConfigurerAdapter { 28 | @Resource 29 | ClientService clientService; 30 | @Resource 31 | UserService userService; 32 | @Resource 33 | AuthenticationManager authenticationManager; 34 | 35 | /** 36 | * 加密方式 37 | */ 38 | @Bean 39 | public PasswordEncoder passwordEncoder() { 40 | return new BCryptPasswordEncoder(); 41 | } 42 | 43 | /** 44 | * jwt类型token转换器 45 | */ 46 | @Bean 47 | public AccessTokenConverter accessTokenConverter() { 48 | return new JwtAccessTokenConverter(); 49 | } 50 | 51 | /** 52 | * token存储方式 53 | * fixme 测试时用内存级,生产以及多实例环境应该使用redis等方式 54 | */ 55 | @Bean 56 | public TokenStore tokenStore() { 57 | return new InMemoryTokenStore(); 58 | } 59 | 60 | /** 61 | * 授权服务器安全相关配置 62 | */ 63 | @Override 64 | public void configure(AuthorizationServerSecurityConfigurer security) { 65 | security 66 | // 配置加密方式 67 | .passwordEncoder(passwordEncoder()) 68 | // jwt单体服务模式一般不会用到,在资源服务器常用 69 | .checkTokenAccess("isAuthenticated()"); 70 | } 71 | 72 | /** 73 | * 客户端相关配置 74 | */ 75 | @Override 76 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 77 | // 配置客户端详情实现类 78 | clients.withClientDetails(clientService); 79 | } 80 | 81 | /** 82 | * 授权服务器默认接口配置 83 | */ 84 | @Override 85 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) { 86 | endpoints 87 | // 配置使用jwt进行token转换 88 | .accessTokenConverter(accessTokenConverter()) 89 | // 由于要使用password方式获取token,所以需要authenticationManager 90 | .authenticationManager(authenticationManager) 91 | // 配置用户详情实现类 92 | .userDetailsService(userService) 93 | // 配置token存储方式 94 | .tokenStore(tokenStore()) 95 | ; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /demo-frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": false 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "demo-frontend": { 10 | "projectType": "application", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | }, 15 | "@schematics/angular:application": { 16 | "strict": true 17 | } 18 | }, 19 | "root": "", 20 | "sourceRoot": "src", 21 | "prefix": "app", 22 | "architect": { 23 | "build": { 24 | "builder": "@angular-devkit/build-angular:browser", 25 | "options": { 26 | "outputPath": "dist/demo-frontend", 27 | "index": "src/index.html", 28 | "main": "src/main.ts", 29 | "polyfills": "src/polyfills.ts", 30 | "tsConfig": "tsconfig.app.json", 31 | "inlineStyleLanguage": "scss", 32 | "assets": [ 33 | "src/favicon.ico", 34 | "src/assets", 35 | { 36 | "glob": "**/*", 37 | "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/", 38 | "output": "/assets/" 39 | } 40 | ], 41 | "styles": [ 42 | "src/theme.less", 43 | "src/styles.scss" 44 | ], 45 | "scripts": [] 46 | }, 47 | "configurations": { 48 | "production": { 49 | "budgets": [ 50 | { 51 | "type": "initial", 52 | "maximumWarning": "500kb", 53 | "maximumError": "1mb" 54 | }, 55 | { 56 | "type": "anyComponentStyle", 57 | "maximumWarning": "2kb", 58 | "maximumError": "4kb" 59 | } 60 | ], 61 | "fileReplacements": [ 62 | { 63 | "replace": "src/environments/environment.ts", 64 | "with": "src/environments/environment.prod.ts" 65 | } 66 | ], 67 | "outputHashing": "all" 68 | }, 69 | "development": { 70 | "buildOptimizer": false, 71 | "optimization": false, 72 | "vendorChunk": true, 73 | "extractLicenses": false, 74 | "sourceMap": true, 75 | "namedChunks": true 76 | } 77 | }, 78 | "defaultConfiguration": "production" 79 | }, 80 | "serve": { 81 | "builder": "@angular-devkit/build-angular:dev-server", 82 | "configurations": { 83 | "production": { 84 | "browserTarget": "demo-frontend:build:production" 85 | }, 86 | "development": { 87 | "browserTarget": "demo-frontend:build:development" 88 | } 89 | }, 90 | "defaultConfiguration": "development" 91 | }, 92 | "extract-i18n": { 93 | "builder": "@angular-devkit/build-angular:extract-i18n", 94 | "options": { 95 | "browserTarget": "demo-frontend:build" 96 | } 97 | }, 98 | "test": { 99 | "builder": "@angular-devkit/build-angular:karma", 100 | "options": { 101 | "main": "src/test.ts", 102 | "polyfills": "src/polyfills.ts", 103 | "tsConfig": "tsconfig.spec.json", 104 | "karmaConfig": "karma.conf.js", 105 | "inlineStyleLanguage": "scss", 106 | "assets": [ 107 | "src/favicon.ico", 108 | "src/assets" 109 | ], 110 | "styles": [ 111 | "src/styles.scss" 112 | ], 113 | "scripts": [] 114 | } 115 | } 116 | } 117 | } 118 | }, 119 | "defaultProject": "demo-frontend" 120 | } 121 | -------------------------------------------------------------------------------- /resource-server/src/test/java/com/example/user/UserControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.example.user; 2 | 3 | import cn.hutool.json.JSONUtil; 4 | import com.example.config.ResourceConfig; 5 | import com.example.config.TestResourceConfig; 6 | import com.example.user.vo.UserVO; 7 | import org.junit.jupiter.api.Test; 8 | import org.mockito.Mockito; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.context.annotation.Import; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.security.test.context.support.WithMockUser; 14 | import org.springframework.test.context.ActiveProfiles; 15 | import org.springframework.test.web.servlet.MockMvc; 16 | 17 | import javax.annotation.Resource; 18 | import java.util.Arrays; 19 | 20 | import static org.hamcrest.Matchers.hasSize; 21 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 22 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 23 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 24 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 25 | 26 | 27 | @WebMvcTest 28 | @ActiveProfiles("unit-test") 29 | @Import({ResourceConfig.class, TestResourceConfig.class}) 30 | class UserControllerTest { 31 | @Resource 32 | MockMvc mockMvc; 33 | @MockBean 34 | UserService userService; 35 | 36 | /** 37 | * 用户列表 38 | */ 39 | @Test 40 | @WithMockUser(authorities = {"showUserList"}) 41 | void userList() throws Exception { 42 | Mockito.when(userService.userList()).thenReturn(Arrays.asList(new UserVO(), new UserVO())); 43 | mockMvc.perform(get("/user")) 44 | .andExpect(status().isOk()) 45 | .andExpect(jsonPath("$").isArray()) 46 | .andExpect(jsonPath("$", hasSize(2))) 47 | ; 48 | } 49 | 50 | @Test 51 | void userList_401() throws Exception { 52 | mockMvc.perform(get("/user")) 53 | .andExpect(status().isUnauthorized()) 54 | ; 55 | } 56 | 57 | @Test 58 | @WithMockUser 59 | void userList_403() throws Exception { 60 | mockMvc.perform(get("/user")) 61 | .andExpect(status().isForbidden()) 62 | ; 63 | } 64 | 65 | @Test 66 | void showUserDetail_401() throws Exception { 67 | mockMvc.perform(get("/user/details/{id}", 1)) 68 | .andExpect(status().isUnauthorized()) 69 | ; 70 | } 71 | 72 | @Test 73 | @WithMockUser 74 | void showUserDetail_403() throws Exception { 75 | mockMvc.perform(get("/user/details/{id}", 1)) 76 | .andExpect(status().isForbidden()) 77 | ; 78 | } 79 | 80 | @Test 81 | @WithMockUser(authorities = {"showUserDetail"}) 82 | void showUserDetail() throws Exception { 83 | UserVO userVO = new UserVO(); 84 | userVO.setId(1); 85 | userVO.setUsername("test"); 86 | userVO.setAge(12); 87 | userVO.setEmail("test@test.com"); 88 | Mockito.when(userService.showUserDetail(1)).thenReturn(userVO); 89 | mockMvc.perform(get("/user/details/{id}", 1)) 90 | .andExpect(status().isOk()) 91 | .andExpect(jsonPath("$.id").value(1)) 92 | .andExpect(jsonPath("$.username").value("test")) 93 | .andExpect(jsonPath("$.age").value(12)) 94 | .andExpect(jsonPath("$.email").value("test@test.com")) 95 | ; 96 | } 97 | 98 | @Test 99 | @WithMockUser(authorities = {"resetUserPassword"}) 100 | void resetUserPassword() throws Exception { 101 | String content = JSONUtil.createObj() 102 | .set("username", "username") 103 | .set("password", "password") 104 | .toString(); 105 | mockMvc.perform(post("/user/resetUserPassword") 106 | .contentType(MediaType.APPLICATION_JSON) 107 | .content(content)) 108 | .andExpect(status().isOk()) 109 | ; 110 | } 111 | 112 | @Test 113 | void resetUserPassword_401() throws Exception { 114 | String content = JSONUtil.createObj() 115 | .set("username", "username") 116 | .set("password", "password") 117 | .toString(); 118 | mockMvc.perform(post("/user/resetUserPassword") 119 | .contentType(MediaType.APPLICATION_JSON) 120 | .content(content)) 121 | .andExpect(status().isUnauthorized()) 122 | ; 123 | } 124 | 125 | @Test 126 | @WithMockUser 127 | void resetUserPassword_403() throws Exception { 128 | String content = JSONUtil.createObj() 129 | .set("username", "username") 130 | .set("password", "password") 131 | .toString(); 132 | mockMvc.perform(post("/user/resetUserPassword") 133 | .contentType(MediaType.APPLICATION_JSON) 134 | .content(content)) 135 | .andExpect(status().isForbidden()) 136 | ; 137 | } 138 | } -------------------------------------------------------------------------------- /resource-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.6.4 9 | 10 | 11 | com.example 12 | resource-server 13 | 0.0.1-SNAPSHOT 14 | MultipleService::ResourceServer 15 | 资源(客户端)服务器 16 | 17 | 18 | 11 19 | 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-security 26 | 27 | 28 | org.springframework.security 29 | spring-security-test 30 | test 31 | 32 | 33 | org.springframework.security.oauth 34 | spring-security-oauth2 35 | 2.5.1.RELEASE 36 | 37 | 38 | org.springframework.security.oauth.boot 39 | spring-security-oauth2-autoconfigure 40 | 2.6.3 41 | 42 | 43 | 44 | 45 | mysql 46 | mysql-connector-java 47 | runtime 48 | 49 | 50 | tk.mybatis 51 | mapper-spring-boot-starter 52 | 4.2.1 53 | 54 | 55 | org.mybatis.spring.boot 56 | mybatis-spring-boot-starter-test 57 | 2.2.2 58 | test 59 | 60 | 61 | org.mybatis.spring.boot 62 | mybatis-spring-boot-autoconfigure 63 | 2.2.2 64 | test 65 | 66 | 67 | com.h2database 68 | h2 69 | test 70 | 71 | 72 | 73 | 74 | cn.hutool 75 | hutool-all 76 | 5.7.22 77 | 78 | 79 | org.apache.httpcomponents 80 | httpclient 81 | 82 | 83 | org.springframework.boot 84 | spring-boot-starter-validation 85 | 86 | 87 | org.springframework.boot 88 | spring-boot-starter-web 89 | 90 | 91 | org.projectlombok 92 | lombok 93 | true 94 | 95 | 96 | org.springframework.boot 97 | spring-boot-starter-actuator 98 | 99 | 100 | org.springframework.boot 101 | spring-boot-starter-test 102 | test 103 | 104 | 105 | 106 | 107 | 108 | 109 | org.springframework.boot 110 | spring-boot-maven-plugin 111 | 112 | 113 | 114 | org.projectlombok 115 | lombok 116 | 117 | 118 | 119 | 120 | 121 | org.mybatis.generator 122 | mybatis-generator-maven-plugin 123 | 1.3.6 124 | 125 | 126 | ${basedir}/src/main/resources/generator/generatorConfig.xml 127 | 128 | true 129 | 130 | 131 | 132 | mysql 133 | mysql-connector-java 134 | 8.0.28 135 | 136 | 137 | tk.mybatis 138 | mapper 139 | 4.2.1 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | aliyun 149 | https://maven.aliyun.com/repository/central 150 | 151 | 152 | 153 | 154 | aliyun 155 | https://maven.aliyun.com/repository/central 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /authorization-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.6.4 9 | 10 | 11 | com.example 12 | authorization-server 13 | 0.0.1-SNAPSHOT 14 | MultipleService::AuthorizationServer 15 | 认证服务器 16 | 17 | 18 | 11 19 | 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-security 26 | 27 | 28 | org.springframework.security 29 | spring-security-test 30 | test 31 | 32 | 33 | org.springframework.security.oauth 34 | spring-security-oauth2 35 | 2.5.1.RELEASE 36 | 37 | 38 | org.springframework.security.oauth.boot 39 | spring-security-oauth2-autoconfigure 40 | 2.6.3 41 | 42 | 43 | 44 | 45 | mysql 46 | mysql-connector-java 47 | runtime 48 | 49 | 50 | tk.mybatis 51 | mapper-spring-boot-starter 52 | 4.2.1 53 | 54 | 55 | org.mybatis.spring.boot 56 | mybatis-spring-boot-starter-test 57 | 2.2.2 58 | test 59 | 60 | 61 | org.mybatis.spring.boot 62 | mybatis-spring-boot-autoconfigure 63 | 2.2.2 64 | test 65 | 66 | 67 | com.h2database 68 | h2 69 | test 70 | 71 | 72 | 73 | 74 | cn.hutool 75 | hutool-all 76 | 5.7.22 77 | 78 | 79 | org.apache.httpcomponents 80 | httpclient 81 | 82 | 83 | org.springframework.boot 84 | spring-boot-starter-validation 85 | 86 | 87 | org.springframework.boot 88 | spring-boot-starter-web 89 | 90 | 91 | org.projectlombok 92 | lombok 93 | true 94 | 95 | 96 | org.springframework.boot 97 | spring-boot-starter-actuator 98 | 99 | 100 | org.springframework.boot 101 | spring-boot-starter-test 102 | test 103 | 104 | 105 | 106 | 107 | 108 | 109 | org.springframework.boot 110 | spring-boot-maven-plugin 111 | 112 | 113 | 114 | org.projectlombok 115 | lombok 116 | 117 | 118 | 119 | 120 | 121 | org.mybatis.generator 122 | mybatis-generator-maven-plugin 123 | 1.3.6 124 | 125 | 126 | ${basedir}/src/main/resources/generator/generatorConfig.xml 127 | 128 | true 129 | 130 | 131 | 132 | mysql 133 | mysql-connector-java 134 | 8.0.28 135 | 136 | 137 | tk.mybatis 138 | mapper 139 | 4.2.1 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | aliyun 149 | https://maven.aliyun.com/repository/central 150 | 151 | 152 | 153 | 154 | aliyun 155 | https://maven.aliyun.com/repository/central 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /resource-server/src/test/java/com/example/user/UserProcessTest.java: -------------------------------------------------------------------------------- 1 | package com.example.user; 2 | 3 | import cn.hutool.json.JSONUtil; 4 | import com.example.config.TestResourceConfig; 5 | import com.example.generator.mapper.UserMapper; 6 | import com.example.generator.model.User; 7 | import com.example.user.vo.ResetUserPasswordVO; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.context.annotation.Import; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.security.crypto.password.PasswordEncoder; 14 | import org.springframework.security.test.context.support.WithMockUser; 15 | import org.springframework.test.context.ActiveProfiles; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | 18 | import javax.annotation.Resource; 19 | import java.security.Principal; 20 | 21 | import static org.hamcrest.Matchers.hasSize; 22 | import static org.junit.jupiter.api.Assertions.assertTrue; 23 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 25 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 26 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 27 | 28 | /** 29 | * 用户接口测试 30 | */ 31 | @SpringBootTest 32 | @AutoConfigureMockMvc 33 | @ActiveProfiles("unit-test") 34 | @Import(TestResourceConfig.class) 35 | public class UserProcessTest { 36 | @Resource 37 | MockMvc mockMvc; 38 | @Resource 39 | UserMapper userMapper; 40 | @Resource 41 | PasswordEncoder passwordEncoder; 42 | 43 | /** 44 | * {@link UserController#userList()} 45 | */ 46 | @Test 47 | @WithMockUser(authorities = {"showUserList"}) 48 | void userList() throws Exception { 49 | mockMvc.perform(get("/user")) 50 | .andExpect(jsonPath("$").isArray()) 51 | .andExpect(jsonPath("$", hasSize(2))) 52 | 53 | .andExpect(jsonPath("$[0].id").value(1)) 54 | .andExpect(jsonPath("$[0].userId").value(1)) 55 | .andExpect(jsonPath("$[0].name").value("测试用户")) 56 | .andExpect(jsonPath("$[0].age").value(12)) 57 | .andExpect(jsonPath("$[0].email").value("test@test.com")) 58 | .andExpect(jsonPath("$[0].gender").value(1)) 59 | .andExpect(jsonPath("$[0].username").value("user")) 60 | 61 | .andExpect(jsonPath("$[1].id").value(2)) 62 | .andExpect(jsonPath("$[1].userId").value(2)) 63 | .andExpect(jsonPath("$[1].name").value("管理员用户")) 64 | .andExpect(jsonPath("$[1].age").value(66)) 65 | .andExpect(jsonPath("$[1].email").value("admin@test.com")) 66 | .andExpect(jsonPath("$[1].gender").value(2)) 67 | .andExpect(jsonPath("$[1].username").value("admin")) 68 | ; 69 | } 70 | 71 | /** 72 | * {@link UserController#showUserDetail(Integer)} 73 | */ 74 | @Test 75 | @WithMockUser(authorities = {"showUserDetail"}) 76 | void showUserDetail() throws Exception { 77 | mockMvc.perform(get("/user/details/{id}", 1)) 78 | .andExpect(jsonPath("$").isNotEmpty()) 79 | .andExpect(jsonPath("$").isMap()) 80 | .andExpect(jsonPath("$.id").value(1)) 81 | .andExpect(jsonPath("$.userId").value(1)) 82 | .andExpect(jsonPath("$.name").value("测试用户")) 83 | .andExpect(jsonPath("$.age").value(12)) 84 | .andExpect(jsonPath("$.email").value("test@test.com")) 85 | .andExpect(jsonPath("$.gender").value(1)) 86 | .andExpect(jsonPath("$.username").value("user")) 87 | ; 88 | mockMvc.perform(get("/user/details/{id}", 2)) 89 | .andExpect(jsonPath("$").isNotEmpty()) 90 | .andExpect(jsonPath("$").isMap()) 91 | .andExpect(jsonPath("$.id").value(2)) 92 | .andExpect(jsonPath("$.userId").value(2)) 93 | .andExpect(jsonPath("$.name").value("管理员用户")) 94 | .andExpect(jsonPath("$.age").value(66)) 95 | .andExpect(jsonPath("$.email").value("admin@test.com")) 96 | .andExpect(jsonPath("$.gender").value(2)) 97 | .andExpect(jsonPath("$.username").value("admin")) 98 | ; 99 | } 100 | 101 | /** 102 | * {@link UserController#resetUserPassword(ResetUserPasswordVO)} 103 | */ 104 | @Test 105 | @WithMockUser(authorities = {"resetUserPassword"}) 106 | void resetUserPassword() throws Exception { 107 | User user = userMapper.selectByPrimaryKey(1); 108 | assertTrue(passwordEncoder.matches("user", user.getPassword())); 109 | String content = JSONUtil.createObj() 110 | .set("username", "user") 111 | .set("password", "user1") 112 | .toString(); 113 | mockMvc.perform(post("/user/resetUserPassword") 114 | .contentType(MediaType.APPLICATION_JSON) 115 | .content(content) 116 | ).andExpect(status().isOk()) 117 | ; 118 | User user1 = userMapper.selectByPrimaryKey(1); 119 | assertTrue(passwordEncoder.matches("user1", user1.getPassword())); 120 | } 121 | 122 | /** 123 | * {@link UserController#showPersonalRole(Principal)} 124 | */ 125 | @Test 126 | @WithMockUser(username = "user", authorities = {"showPersonalRole"}) 127 | void showPersonalRole() throws Exception { 128 | mockMvc.perform(get("/user/showPersonalRole")) 129 | .andExpect(status().isOk()) 130 | .andExpect(jsonPath("$").isArray()) 131 | .andExpect(jsonPath("$", hasSize(1))) 132 | .andExpect(jsonPath("$[0]").value("普通用户")) 133 | ; 134 | } 135 | 136 | /** 137 | * {@link UserController#showPersonalRole(Principal)} 138 | */ 139 | @Test 140 | @WithMockUser(username = "admin", authorities = {"showPersonalRole"}) 141 | void showPersonalRole_1() throws Exception { 142 | mockMvc.perform(get("/user/showPersonalRole")) 143 | .andExpect(status().isOk()) 144 | .andExpect(jsonPath("$").isArray()) 145 | .andExpect(jsonPath("$", hasSize(2))) 146 | .andExpect(jsonPath("$[0]").value("普通用户")) 147 | .andExpect(jsonPath("$[1]").value("系统管理员")) 148 | ; 149 | } 150 | 151 | /** 152 | * {@link UserController#showPersonalDetail(Principal)} 153 | */ 154 | @Test 155 | @WithMockUser(username = "user", authorities = {"showPersonalDetail"}) 156 | void showPersonalDetail() throws Exception { 157 | mockMvc.perform(get("/user/showPersonalDetail")) 158 | .andExpect(status().isOk()) 159 | .andExpect(jsonPath("$.id").value(1)) 160 | .andExpect(jsonPath("$.userId").value(1)) 161 | .andExpect(jsonPath("$.name").value("测试用户")) 162 | .andExpect(jsonPath("$.age").value(12)) 163 | .andExpect(jsonPath("$.email").value("test@test.com")) 164 | .andExpect(jsonPath("$.gender").value(1)) 165 | ; 166 | } 167 | 168 | /** 169 | * {@link UserController#showPersonalDetail(Principal)} 170 | */ 171 | @Test 172 | @WithMockUser(username = "admin", authorities = {"showPersonalDetail"}) 173 | void showPersonalDetail_1() throws Exception { 174 | mockMvc.perform(get("/user/showPersonalDetail")) 175 | .andExpect(status().isOk()) 176 | .andExpect(jsonPath("$.id").value(2)) 177 | .andExpect(jsonPath("$.userId").value(2)) 178 | .andExpect(jsonPath("$.name").value("管理员用户")) 179 | .andExpect(jsonPath("$.age").value(66)) 180 | .andExpect(jsonPath("$.email").value("admin@test.com")) 181 | .andExpect(jsonPath("$.gender").value(2)) 182 | ; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /authorization-server/src/test/java/com/example/auth/AuthTest.java: -------------------------------------------------------------------------------- 1 | package com.example.auth; 2 | 3 | import cn.hutool.core.map.MapUtil; 4 | import cn.hutool.core.util.ReUtil; 5 | import cn.hutool.json.JSONObject; 6 | import com.example.utils.TestRequestUtils; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.boot.test.web.client.TestRestTemplate; 10 | import org.springframework.http.HttpEntity; 11 | import org.springframework.http.HttpMethod; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.test.context.ActiveProfiles; 14 | import org.springframework.util.MultiValueMap; 15 | 16 | import javax.annotation.Resource; 17 | import java.util.HashMap; 18 | import java.util.Map; 19 | 20 | import static org.junit.jupiter.api.Assertions.*; 21 | import static org.springframework.security.oauth2.common.util.OAuth2Utils.*; 22 | 23 | /** 24 | * 权限测试 25 | */ 26 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 27 | @ActiveProfiles("unit-test") 28 | public class AuthTest { 29 | @Resource 30 | TestRestTemplate testRestTemplate; 31 | public static final String TEST_CLIENT = "client"; 32 | public static final String TEST_SECRET = "secret"; 33 | public static final String TEST_USERNAME = "user"; 34 | public static final String TEST_PASSWORD = "user"; 35 | 36 | /** 37 | * password方式获取token 38 | */ 39 | @Test 40 | void getToken_password() { 41 | // 1.测试获取token接口 42 | HttpEntity> entity = TestRequestUtils.urlEncodedRequest() 43 | .put(GRANT_TYPE, "password") 44 | .put("username", TEST_USERNAME) 45 | .put("password", TEST_PASSWORD) 46 | .build(); 47 | ResponseEntity exchange = testRestTemplate.withBasicAuth(TEST_CLIENT, TEST_SECRET) 48 | .exchange("/oauth/token", HttpMethod.POST, entity, JSONObject.class); 49 | 50 | // 验证token 51 | assertTokenResult(exchange); 52 | 53 | assertNotNull(exchange.getBody()); 54 | String token = exchange.getBody().getStr("access_token"); 55 | 56 | // 2.测试check_token接口 57 | // jwt单体服务模式一般不会用到,在资源服务器常用 58 | HttpEntity> entity1 = TestRequestUtils.urlEncodedRequest() 59 | .put("token", token).build(); 60 | ResponseEntity exchange1 = testRestTemplate 61 | .withBasicAuth(TEST_CLIENT, TEST_SECRET) 62 | .exchange("/oauth/check_token", HttpMethod.POST, entity1, JSONObject.class); 63 | 64 | // 验证结果 65 | JSONObject body1 = exchange1.getBody(); 66 | assertEquals(200, exchange1.getStatusCodeValue()); 67 | assertNotNull(body1); 68 | assertEquals(TEST_USERNAME, body1.getStr("user_name")); 69 | assertTrue(body1.getBool("active")); 70 | assertEquals(3, body1.getJSONArray("authorities").size()); 71 | assertEquals(TEST_CLIENT, body1.getStr("client_id")); 72 | assertEquals(1, body1.getJSONArray("scope").size()); 73 | assertNotNull(body1.getInt("exp")); 74 | assertNotNull(body1.getStr("jti")); 75 | 76 | String refreshToken = exchange.getBody().getStr("refresh_token"); 77 | 78 | // 3.测试刷新token接口 79 | HttpEntity> entity2 = TestRequestUtils.urlEncodedRequest() 80 | .put(GRANT_TYPE, "refresh_token") 81 | .put("refresh_token", refreshToken) 82 | .build(); 83 | ResponseEntity exchange2 = testRestTemplate.withBasicAuth(TEST_CLIENT, TEST_SECRET) 84 | .exchange("/oauth/token", HttpMethod.POST, entity2, JSONObject.class); 85 | 86 | // 验证返回token 87 | assertTokenResult(exchange2); 88 | } 89 | 90 | private void assertTokenResult(ResponseEntity exchange) { 91 | JSONObject body = exchange.getBody(); 92 | 93 | assertEquals(200, exchange.getStatusCodeValue()); 94 | assertNotNull(body); 95 | assertNotNull(body.get("token_type")); 96 | assertNotNull(body.get("access_token")); 97 | assertNotNull(body.get("refresh_token")); 98 | assertNotNull(body.get("scope")); 99 | assertNotNull(body.get("expires_in")); 100 | assertNotNull(body.get("jti")); 101 | } 102 | 103 | /** 104 | * code方式获取token 105 | */ 106 | @Test 107 | void getToken_code() { 108 | // 1.访问权限url 109 | String redirectUri = "http://localhost:4200/login"; 110 | HttpEntity> entity = TestRequestUtils.urlEncodedRequest() 111 | .put(RESPONSE_TYPE, "code") 112 | .put(REDIRECT_URI, redirectUri) 113 | .put(CLIENT_ID, TEST_CLIENT) 114 | .build(); 115 | 116 | ResponseEntity exchange = testRestTemplate.withBasicAuth(TEST_USERNAME, TEST_PASSWORD) 117 | .exchange("/oauth/authorize", HttpMethod.POST, entity, String.class); 118 | 119 | assertTrue(exchange.getStatusCodeValue() == 303 || exchange.getStatusCodeValue() == 302); 120 | assertNotNull(exchange.getHeaders().getLocation()); 121 | String query = exchange.getHeaders().getLocation().getQuery(); 122 | assertTrue(ReUtil.contains("code=", query)); 123 | 124 | // 在跳转页面url中拿到code 125 | String code = ReUtil.findAllGroup0("(?<=code=)\\S+", query).get(0); 126 | 127 | // 2.通过code获取token 128 | HttpEntity> entity1 = TestRequestUtils.urlEncodedRequest() 129 | .put(GRANT_TYPE, "authorization_code") 130 | .put(REDIRECT_URI, redirectUri) 131 | .put("code", code) 132 | .build(); 133 | ResponseEntity exchange1 = testRestTemplate.withBasicAuth(TEST_CLIENT, TEST_SECRET) 134 | .exchange("/oauth/token", HttpMethod.POST, entity1, JSONObject.class); 135 | 136 | // 校验返回的token 137 | assertTokenResult(exchange1); 138 | } 139 | 140 | /** 141 | * 注册 142 | */ 143 | @Test 144 | void register() { 145 | // 1.测试注册请求---只传用户名 146 | HttpEntity entity = TestRequestUtils.jsonRequest().set("username", "testUsername").build(); 147 | ResponseEntity exchange = testRestTemplate.exchange("/oauth/register", HttpMethod.POST, entity, JSONObject.class); 148 | 149 | // 校验返回错误消息 150 | assertEquals(400, exchange.getStatusCodeValue()); 151 | JSONObject body = exchange.getBody(); 152 | assertNotNull(body); 153 | assertEquals("密码不能为空", body.getByPath("errors[0].defaultMessage")); 154 | 155 | // 2.测试注册请求---只传密码 156 | entity = TestRequestUtils.jsonRequest().set("password", "testPassword").build(); 157 | exchange = testRestTemplate.exchange("/oauth/register", HttpMethod.POST, entity, JSONObject.class); 158 | 159 | // 校验返回错误消息 160 | assertEquals(400, exchange.getStatusCodeValue()); 161 | body = exchange.getBody(); 162 | assertNotNull(body); 163 | assertEquals("用户名不能为空", body.getByPath("errors[0].defaultMessage")); 164 | 165 | // 3.测试注册请求---正确的 166 | entity = TestRequestUtils.jsonRequest() 167 | .set("username", "testUsername") 168 | .set("password", "testPassword").build(); 169 | exchange = testRestTemplate.exchange("/oauth/register", HttpMethod.POST, entity, JSONObject.class); 170 | 171 | // 校验注册成功 172 | assertEquals(200, exchange.getStatusCodeValue()); 173 | 174 | // 4.使用新注册的用户获取token 175 | HttpEntity> entity1 = TestRequestUtils.urlEncodedRequest() 176 | .put(GRANT_TYPE, "password") 177 | .put("username", "testUsername") 178 | .put("password", "testPassword") 179 | .build(); 180 | exchange = testRestTemplate.withBasicAuth(TEST_CLIENT, TEST_SECRET) 181 | .exchange("/oauth/token", HttpMethod.POST, entity1, JSONObject.class); 182 | 183 | // 校验返回的token 184 | assertTokenResult(exchange); 185 | 186 | // 5.使用重复的用户名注册 187 | exchange = testRestTemplate.exchange("/oauth/register", HttpMethod.POST, entity, JSONObject.class); 188 | 189 | // 校验返回的错误消息 190 | assertEquals(400, exchange.getStatusCodeValue()); 191 | body = exchange.getBody(); 192 | assertNotNull(body); 193 | assertEquals("用户名已存在", body.getByPath("errors[0].defaultMessage")); 194 | } 195 | 196 | /** 197 | * 注销 198 | */ 199 | @Test 200 | void logout() { 201 | // 1.获取用户token 202 | HttpEntity> entity = TestRequestUtils.urlEncodedRequest() 203 | .put(GRANT_TYPE, "password") 204 | .put("username", TEST_USERNAME) 205 | .put("password", TEST_PASSWORD) 206 | .build(); 207 | ResponseEntity exchange = testRestTemplate.withBasicAuth(TEST_CLIENT, TEST_SECRET) 208 | .exchange("/oauth/token", HttpMethod.POST, entity, JSONObject.class); 209 | // 校验返回结果 210 | assertTokenResult(exchange); 211 | // 暂存token 212 | assertNotNull(exchange.getBody()); 213 | String token = exchange.getBody().getStr("access_token"); 214 | // 2.通过用户身份调用注销接口 215 | String redirectUri = "http://localhost:4200/login"; 216 | Map variables = MapUtil.builder(new HashMap()) 217 | .put("token", token) 218 | .put("redirect", redirectUri) 219 | .build(); 220 | // fixme 这里不知道为啥必须要加上httpclient依赖才能捕捉到30x的状态码,否则会报错 221 | ResponseEntity exchange1 = testRestTemplate.getForEntity("/oauth/logout?token={token}&redirect={redirect}", 222 | String.class, variables); 223 | // 校验返回结果 224 | assertTrue(exchange1.getStatusCodeValue() == 303 || exchange1.getStatusCodeValue() == 302); 225 | assertNotNull(exchange1.getHeaders().getLocation()); 226 | assertEquals(redirectUri, exchange1.getHeaders().getLocation().toString()); 227 | // 3.再次使用token调用check_token接口测试token是否失效 228 | HttpEntity> entity1 = TestRequestUtils.urlEncodedRequest() 229 | .put("token", token).build(); 230 | ResponseEntity exchange2 = testRestTemplate 231 | .withBasicAuth(TEST_CLIENT, TEST_SECRET) 232 | .exchange("/oauth/check_token", HttpMethod.POST, entity1, JSONObject.class); 233 | // 校验返回的错误信息 234 | assertEquals(400, exchange2.getStatusCodeValue()); 235 | assertNotNull(exchange2.getBody()); 236 | assertEquals("invalid_token", exchange2.getBody().getStr("error")); 237 | } 238 | 239 | /** 240 | * 修改密码 241 | */ 242 | @Test 243 | void changePassword() { 244 | // 1.获取用户token 245 | HttpEntity> entity = TestRequestUtils.urlEncodedRequest() 246 | .put(GRANT_TYPE, "password") 247 | .put("username", TEST_USERNAME) 248 | .put("password", TEST_PASSWORD) 249 | .build(); 250 | ResponseEntity exchange = testRestTemplate.withBasicAuth(TEST_CLIENT, TEST_SECRET) 251 | .exchange("/oauth/token", HttpMethod.POST, entity, JSONObject.class); 252 | // 校验返回结果 253 | assertTokenResult(exchange); 254 | // 暂存token 255 | assertNotNull(exchange.getBody()); 256 | String token = exchange.getBody().getStr("access_token"); 257 | // 2.修改用户密码 258 | HttpEntity entity1 = TestRequestUtils.jsonRequest() 259 | .set("token", token) 260 | .set("password", "changePasswordNewPwd") 261 | .build(); 262 | ResponseEntity exchange1 = testRestTemplate.exchange("/oauth/changePassword", 263 | HttpMethod.POST, entity1, JSONObject.class); 264 | // 校验返回结果 265 | assertEquals(200, exchange1.getStatusCodeValue()); 266 | // 3.再次使用token调用check_token接口测试token是否失效 267 | HttpEntity> entity2 = TestRequestUtils.urlEncodedRequest() 268 | .put("token", token).build(); 269 | ResponseEntity exchange2 = testRestTemplate 270 | .withBasicAuth(TEST_CLIENT, TEST_SECRET) 271 | .exchange("/oauth/check_token", HttpMethod.POST, entity2, JSONObject.class); 272 | // 校验返回的错误信息 273 | assertEquals(400, exchange2.getStatusCodeValue()); 274 | assertNotNull(exchange2.getBody()); 275 | assertEquals("invalid_token", exchange2.getBody().getStr("error")); 276 | // 4.使用新密码获取token 277 | HttpEntity> entity3 = TestRequestUtils.urlEncodedRequest() 278 | .put(GRANT_TYPE, "password") 279 | .put("username", TEST_USERNAME) 280 | .put("password", "changePasswordNewPwd") 281 | .build(); 282 | ResponseEntity exchange3 = testRestTemplate.withBasicAuth(TEST_CLIENT, TEST_SECRET) 283 | .exchange("/oauth/token", HttpMethod.POST, entity3, JSONObject.class); 284 | // 校验返回结果 285 | assertTokenResult(exchange3); 286 | // 暂存token 287 | assertNotNull(exchange3.getBody()); 288 | String token3 = exchange3.getBody().getStr("access_token"); 289 | // 5.使用新的token调用check_token接口 290 | HttpEntity> entity4 = TestRequestUtils.urlEncodedRequest() 291 | .put("token", token3).build(); 292 | ResponseEntity exchange4 = testRestTemplate 293 | .withBasicAuth(TEST_CLIENT, TEST_SECRET) 294 | .exchange("/oauth/check_token", HttpMethod.POST, entity4, JSONObject.class); 295 | // 校验返回结果 296 | JSONObject body1 = exchange4.getBody(); 297 | assertEquals(200, exchange4.getStatusCodeValue()); 298 | assertNotNull(body1); 299 | assertEquals(TEST_USERNAME, body1.getStr("user_name")); 300 | assertTrue(body1.getBool("active")); 301 | assertEquals(3, body1.getJSONArray("authorities").size()); 302 | assertEquals(TEST_CLIENT, body1.getStr("client_id")); 303 | assertEquals(1, body1.getJSONArray("scope").size()); 304 | assertNotNull(body1.getInt("exp")); 305 | assertNotNull(body1.getStr("jti")); 306 | } 307 | } 308 | --------------------------------------------------------------------------------