Springboot整合oauth2.0

oauth2基本概念

1.什么是oauth2

  1. OAuth2.0介绍 OAuth(Open Authorization)是一个关于授权(authorization)的开放网络标准,允许用户授权第三方 应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他 们数据的所有内容。OAuth在全世界得到广泛应用,目前的版本是2.0版。
  2. OAuth协议:https://tools.ietf.org/html/rfc6749 协议特点:简单:不管是OAuth服务提供者还是应用开发者,都很易于理解与使用; 安全:没有涉及到用户密钥等信息,更安全更灵活; 开放:任何服务提供商都可以实现OAuth,任何软件开发商都可以使用OAuth;

2.为什么用oauth2

业务系统 需要实现几种登录形式

  1. 原生app授权 app登录请求后台接口,为了安全认证,所有请求都带token信息,如果登录验证、 请求后台数据。
  2. 前后端分离单页面应用:前后端分离框架,前端请求后台数据,需要进行oauth2安全认证,比如 使用vue、react后者h5开发的app
  3. 第三方应用授权登录,比如QQ,微博,微信的授权登录。

3.怎么用oauth2

看代码

4.oauth中的角色

OAuth 定义了四个角色:

4.1 Resource owner 资源拥有者(用户)

能够授予对受保护资源的访问权限的实体。当资源所有者是一个人时,它被称为最终用户

Resource Server 资源服务器

托管受保护资源的服务器,能够使用访问令牌接受和响应受保护资源请求

Client 客户端

代表资源所有者并经其授权发出受保护资源请求的应用程序。“客户”一词确实 不暗示任何特定的实现特征(例如, 应用程序是否在服务器、桌面或其他 设备上执行)。

Authorization server 授权服务器

服务器在成功 验证资源所有者并获得授权后向客户端颁发访问令牌。授权服务器和资源服务器之间的交互超出了本规范的范围。授权服务器 可以是与资源服务器相同的服务器,也可以是单独的实体。 单个授权服务器可以发布多个资源服务器接受的访问令牌。

5.认证流程参考

在这里插入图片描述

6.Oauth2 的四种认证模式的流程

6.1 Authorization Code (授权码模式)

应用场景

这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏

流程

在这里插入图片描述

1:用户访问页面
2:访问的页面将请求重定向到认证服务器
3:认证服务器向用户展示授权页面,等待用户授权
4:用户授权,认证服务器生成一个code和带上client_id发送给应用服务器,然后,应用服务器拿到code,并用client_id去后台查询对应的client_secret
5:将code、client_id、client_secret传给认证服务器换取access_token和refresh_token
6:将access_token和refresh_token传给应用服务器
7:验证token,访问真正的资源页面

6.2 Resource Owner Password Credentials Grant(密码模式)

应用场景

如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为”密码式”(password)

流程

在这里插入图片描述

1:用户访问用页面时,输入第三方认证所需要的信息(QQ/微信账号密码)
2:应用页面那种这个信息去认证服务器授权
3:认证服务器授权通过,拿到token,访问真正的资源页面

6.3 Implicit Grant (隐式授权模式)

应用场景

有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)“隐藏式”(implicit)

流程

在这里插入图片描述

1:用户访问页面时,重定向到认证服务器。
2:认证服务器给用户一个认证页面,等待用户授权。
3:用户授权,认证服务器想应用页面返回Token
4:验证Token,访问真正的资源页面

6.4 Client Credentials Grant (客户端凭证模式)

应用场景

适用于没有前端的命令行应用,即在命令行下请求令牌

流程

在这里插入图片描述

1:用户访问应用客户端
2:通过客户端定义的验证方法,拿到token,无需授权
3:访问资源服务器A
4:拿到一次token就可以畅通无阻的访问其他的资源页面,如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为”密码式”(password)

7.刷新token原理

在这里插入图片描述

8.整合(代码部分,可以跳过)

gitee地址:https://gitee.com/cactuscode/springboot-oauth2.0.git

SpringBoot 2.7 + maven 3.X + JDK 1.8 + MySQL 5.7

8.1MySQL部分

oauth_access_token.sql

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
/*
Navicat Premium Data Transfer

Source Server : 192.168.55.101
Source Server Type : MySQL
Source Server Version : 50734
Source Host : 192.168.55.101:3306
Source Schema : cqc

Target Server Type : MySQL
Target Server Version : 50734
File Encoding : 65001

Date: 20/09/2023 19:39:10
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for oauth_access_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
`token_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`token` blob NULL,
`authentication_id` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`user_name` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authentication` blob NULL,
`refresh_token` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`authentication_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

oauth_client_details.sql

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
43
44
45
46
/*
Navicat Premium Data Transfer

Source Server : 192.168.55.101
Source Server Type : MySQL
Source Server Version : 50734
Source Host : 192.168.55.101:3306
Source Schema : cqc

Target Server Type : MySQL
Target Server Version : 50734
File Encoding : 65001

Date: 20/09/2023 19:39:30
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`access_token_validity` int(11) NULL DEFAULT NULL,
`refresh_token_validity` int(11) NULL DEFAULT NULL,
`additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details` VALUES ('client', '', '$2a$10$vtOTvQNvYfgHZjh1qYEeDOp4KOWRP9Q5Po2vPuv.ZiAEzxP7xqGZm', 'read,write', 'password,refresh_token', '', 'ROLE_ADMIN,ROLE_USER', 7200, 7200, '{}', '');
INSERT INTO `oauth_client_details` VALUES ('client_1', '', '$2a$10$vtOTvQNvYfgHZjh1qYEeDOp4KOWRP9Q5Po2vPuv.ZiAEzxP7xqGZm', 'read,write', 'client_credentials', '', '', 7200, 7200, '{}', '');

SET FOREIGN_KEY_CHECKS = 1;

oauth_refresh_token.sql

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
/*
Navicat Premium Data Transfer

Source Server : 192.168.55.101
Source Server Type : MySQL
Source Server Version : 50734
Source Host : 192.168.55.101:3306
Source Schema : cqc

Target Server Type : MySQL
Target Server Version : 50734
File Encoding : 65001

Date: 20/09/2023 19:39:43
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for oauth_refresh_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`token` blob NULL,
`authentication` blob NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

userinfo.sql

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
/*
Navicat Premium Data Transfer

Source Server : 192.168.55.101
Source Server Type : MySQL
Source Server Version : 50734
Source Host : 192.168.55.101:3306
Source Schema : cqc

Target Server Type : MySQL
Target Server Version : 50734
File Encoding : 65001

Date: 20/09/2023 19:39:54
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for userinfo
-- ----------------------------
DROP TABLE IF EXISTS `userinfo`;
CREATE TABLE `userinfo` (
`user_id` int(11) NOT NULL,
`user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of userinfo
-- ----------------------------
INSERT INTO `userinfo` VALUES (1, 'admin', '$2a$10$qmVzBdt8AUAKzc.lGb8gbuOeh.LIF.52.d6yTzdq9ICgcAiUhuGEa');
INSERT INTO `userinfo` VALUES (2, 'cqc', '$2a$10$DrXszvRHzT/fLCw2nznSluxybk50Mnu/UzqY/.n5JxhVZqm0T7YNe');

-- admin/123456
-- cqc/admin

SET FOREIGN_KEY_CHECKS = 1;

代码工程

image-20230920195658590

pom.xml

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.5.2.RELEASE</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-jwt -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

application.yml

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

server:
port: 8001

spring:
datasource:
url: jdbc:mysql://192.168.55.101:3306/cqc?characterEncoding=utf-8&autoReconnect=true&useSSL=false
username: root
password: Gmmysql_300098
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
minimum-idle: 5
idle-timeout: 600000
maximum-pool-size: 10
auto-commit: true
pool-name: MyHikariCP
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
mybatis-plus:
mapper-locations: classpath:mapper/*.xml


logging:
level:
com.com.cactus.auth2.mapper: debug

UserInfo

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.cactus.auth2.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;


@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("userinfo")
public class UserInfo implements UserDetails {

@TableId
@TableField("user_id")
private Integer userId;
@TableField("user_name")
private String userName;
@TableField("password")
private String password;

//将我们的 权限字符串进行分割,并存到集合中,最后供 AuthenticationProvider 使用
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {

return null;
}

//将我们的 密码 转换成 AuthenticationProvider 能够认识的 password
@Override
public String getPassword() {
return this.password;
}

//将我们的 账户 转换成 AuthenticationProvider 能够认识的 username
@Override
public String getUsername() {
return this.userName;
}

//都返回 true
@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}

UserMapper

1
2
3
4
5
6
7
8
9
10
11
package com.cactus.auth2.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cactus.auth2.entity.UserInfo;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<UserInfo> {
// UserInfo getByUsername(String username);
}

UserAuthService

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
package com.cactus.auth2.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.cactus.auth2.entity.UserInfo;
import com.cactus.auth2.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;


@Component
public class UserAuthService implements UserDetailsService {
@Autowired
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// UserInfo user = userMapper.getByUsername(username);

QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
QueryWrapper<UserInfo> eq = queryWrapper.eq("user_name", username);
UserInfo user = userMapper.selectOne(eq);
if(user == null){
throw new UsernameNotFoundException("not found the user:"+username);
}else{
return user;
}
// if("admin".equals(username)){
// BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// return new UserInfo(1, "admin", encoder.encode("123456"));
// }else{
// throw new UsernameNotFoundException("not found the user:"+username);
// }
}
}

CustomOauthExceptionSerializer

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
43
44
45
46
47
48
package com.cactus.auth2.exception;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Date;
import java.util.Map;

/**
*
* @ClassName: CustomOauthExceptionSerializer
* @Description:password模式错误处理,自定义登录失败异常信息
* @Author majinzhong
* @Date 2023/7/3 16:32
* @Version 1.0
*/
public class CustomOauthExceptionSerializer extends StdSerializer<CustomOauthException> {

private static final long serialVersionUID = 1478842053473472921L;

public CustomOauthExceptionSerializer() {
super(CustomOauthException.class);
}
@Override
public void serialize(CustomOauthException value, JsonGenerator gen, SerializerProvider provider) throws IOException {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

gen.writeStartObject();
gen.writeStringField("error", String.valueOf(value.getHttpErrorCode()));
// gen.writeStringField("message", value.getMessage());
gen.writeStringField("message", "用户名或密码错误");
gen.writeStringField("path", request.getServletPath());
gen.writeStringField("timestamp", String.valueOf(new Date().getTime()));
if (value.getAdditionalInformation()!=null) {
for (Map.Entry<String, String> entry :
value.getAdditionalInformation().entrySet()) {
String key = entry.getKey();
String add = entry.getValue();
gen.writeStringField(key, add);
}
}
gen.writeEndObject();
}
}

CustomOauthException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.cactus.auth2.exception;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;

/**
*
* @ClassName: CustomOauthException
* @Description:password模式错误处理,自定义登录失败异常信息
* @Author
* @Date 2023/7/3 16:32
* @Version 1.0
*/
@JsonSerialize(using = CustomOauthExceptionSerializer.class)
public class CustomOauthException extends OAuth2Exception {
public CustomOauthException(String msg) {
super(msg);
}
}

AuthExceptionEntryPoint

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
package com.cactus.auth2.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
* 自定义AuthExceptionEntryPoint用于tokan校验失败返回信息
* @Author
* @Date 2023/7/3 16:31
* @Version 1.0
*/
public class AuthExceptionEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws ServletException {

Map<String, Object> map = new HashMap<>();
//401 未授权
map.put("error", HttpServletResponse.SC_UNAUTHORIZED);
map.put("message", authException.getMessage());
map.put("path", request.getServletPath());
map.put("timestamp", String.valueOf(new Date().getTime()));
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
try {
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), map);
} catch (Exception e) {
throw new ServletException();
}
}

}

AuthorizationServerConfig

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.cactus.auth2.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import javax.sql.DataSource;

@Configuration
@EnableAuthorizationServer // 这个注解告诉 Spring 这个应用是 OAuth2 的授权服务器//
// 提供/oauth/authorize,/oauth/token,/oauth/check_token,/oauth/confirm_access,/oauth/error
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;

@Autowired
@Qualifier("dataSource")
private DataSource dataSource;

@Autowired
private UserDetailsService userDetailsService;


@Bean
public TokenStore tokenStore() {
// return new InMemoryTokenStore(); //使用内存中的 token store
return new JdbcTokenStore(dataSource); ///使用Jdbctoken store
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

clients.jdbc(dataSource);

// .withClient("client")
// .secret(new BCryptPasswordEncoder().encode("123456"))
// .authorizedGrantTypes("password", "refresh_token")//允许授权范围
//// .authorities("ROLE_ADMIN","ROLE_USER")//客户端可以使用的权限
// .scopes( "read", "write")
// .accessTokenValiditySeconds(7200)
// .refreshTokenValiditySeconds(7200);
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore())
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);//必须设置 UserDetailsService 否则刷新token 时会报错
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();//允许表单登录

}
}

CustomWebResponseExceptionTranslator

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
package com.cactus.auth2.config;

import com.cactus.auth2.exception.CustomOauthException;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.stereotype.Component;

/**
*
* @ClassName: CustomWebResponseExceptionTranslator
* @Description:password模式错误处理,自定义登录失败异常信息
* @Author
* @Date 2023/7/3 16:33
* @Version 1.0
*/
@Component
public class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator {
@Override
public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {

OAuth2Exception oAuth2Exception = (OAuth2Exception) e;
return ResponseEntity
//.status(oAuth2Exception.getHttpErrorCode())
.status(200)
.body(new CustomOauthException(oAuth2Exception.getMessage()));
}
}

ResourceServerConfig

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
package com.cactus.auth2.config;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

@Configuration
@EnableResourceServer //这个类表明了此应用是OAuth2 的资源服务器,此处主要指定了受资源服务器保护的资源链接
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {



@Override
public void configure(HttpSecurity http) throws Exception {

http.csrf().disable()//禁用了 csrf 功能
.authorizeRequests()//限定签名成功的请求
.antMatchers("/decision/**","/govern/**").hasAnyRole("USER","ADMIN")
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/test/**").authenticated()//必须认证过后才可以访问
.anyRequest().permitAll()//其他没有限定的请求,允许随意访问
.and().anonymous();//对于没有配置权限的其他请求允许匿名访问


}

}

WebSecurityConfig

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.cactus.auth2.config;


import com.cactus.auth2.service.UserAuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userDetailsService;

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean() ;
}

/**
* 配置用户签名服务 主要是user-details 机制,
*
* @param auth 签名管理器构造器,用于构建用户具体权限控制
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}


/**
* 用来配置拦截保护的请求
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {

//不拦截 oauth 开放的资源
http.csrf().disable();

http.requestMatchers()//使HttpSecurity接收以"/login/","/oauth/"开头请求。
.antMatchers("/oauth/**", "/login/**", "/logout/**")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and()
.formLogin();
}

}

TestController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.cactus.auth2.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {


@RequestMapping("/test")
public String test() {
return "test";
}
}

Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.cactus.auth2;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@SpringBootApplication
@MapperScan(basePackages = "com.cactus.auth2.mapper")
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);

BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("admin");
System.out.println(password);
}

}

9.测试

此处在oauth_client_details.sql配置了两个客户端,以及登录方式

image-20230920200947671

9.1账号密码登录

1
2
3
4
5
6
7
8
9
10
11
12
POST http://localhost:8001/oauth/token?username=cqc&password=admin&grant_type=password&client_id=client&client_secret=123456&grant_type=refresh_token




{
"access_token": "fFJqdNi5bKN9bbnC67nHrYP4B54",
"token_type": "bearer",
"refresh_token": "3Lzb46zVp3rBFfndNTABLm080zg",
"expires_in": 7199,
"scope": "read write"
}

image-20230920200325572

验证

image-20230920200452101

9.2客户端登录

1
2
3
4
5
6
7
8
POST  http://localhost:8001/oauth/token?grant_type=client_credentials&client_id=client_1&client_secret=123456

{
"access_token": "1EFpbWyx0yYLBFd02LPxiATr2XE",
"token_type": "bearer",
"expires_in": 6931,
"scope": "read write"
}

image-20230920200532755

测试

image-20230920200604860


Springboot整合oauth2.0
https://cai-qichang.github.io/2023/09/20/Springboot整合oauth2-0/
作者
caiqichang
发布于
2023年9月20日
许可协议
BY-蔡奇倡