├── .gitignore
├── README.md
├── pom.xml
└── src
└── main
├── java
└── com
│ └── github
│ └── ealen
│ ├── Oauth2AuthenticatorApplication.java
│ ├── domain
│ ├── entity
│ │ └── OauthAccount.java
│ ├── mapper
│ │ └── OauthAccountMapper.java
│ └── vo
│ │ ├── AccountInfo.java
│ │ └── AuthResp.java
│ └── infra
│ └── config
│ ├── AuthorizationServerConfig.java
│ ├── OauthAccountUserDetails.java
│ ├── OauthAccountUserDetailsService.java
│ ├── OauthClientAccessTokenConfig.java
│ └── WebSecurityConfig.java
└── resources
├── application.yml
├── db
├── data.sql
└── schema.sql
└── mapper
└── OauthAccountMapper.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project exclude paths
2 | /target/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | SpringBoot整合spring-security-oauth2完整实现例子
2 | ========================
3 |
4 |
5 |
6 | 技术栈 : springboot + spring-security + spring-oauth2 + mybatis-plus
7 |
8 | 完整的项目地址 :
9 |
10 | [OAuth2.0](https://oauth.net/2/)是当下最主流的授权机制,如若不清楚什么是OAuth2.0,请移步[Oauth2详解-介绍(一)](https://www.jianshu.com/p/84a4b4a1e833),[OAuth 2.0 的四种方式 - 阮一峰的网络日志](http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html)等文章进行学习。
11 |
12 | 此例子基本完整实现了OAuth2.0四种授权模式。
13 |
14 |
15 | ### 1. 客户端凭证式(此模式不支持刷新令牌)
16 |
17 | 
18 |
19 |
20 | 请求示例 :
21 | ```
22 | POST /oauth/token HTTP/1.1
23 | Host: localhost:8080
24 | Authorization: Basic QUJDOjEyMzQ1Ng==
25 | Content-Type: application/x-www-form-urlencoded
26 | Content-Length: 29
27 |
28 | grant_type=client_credentials
29 | ```
30 |
31 | 此模式获取令牌接口 `grant_type`固定传值 client_credentials,客户端认证信息通过basic认证方式。
32 |
33 |
34 | ### 2. 用户密码模式
35 |
36 | 请求示例 :
37 |
38 | 
39 |
40 |
41 | ```
42 | POST /oauth/token HTTP/1.1
43 | Host: localhost:8080
44 | Authorization: Basic QUJDOjEyMzQ1Ng==
45 | Content-Type: application/x-www-form-urlencoded
46 | Content-Length: 52
47 |
48 | grant_type=password&username=ealenxie&password=admin
49 | ```
50 | 此模式获取令牌接口 `grant_type`固定传值 password并且携带用户名密码进行认证。~~~~
51 |
52 |
53 | ### 3. 授权码模式
54 |
55 | 此模式过程相对要复杂一些,首先需要认证过的用户先进行授权,获取到授权码code(通过回调url传递回来)之后,再向认证授权中心通过code去获取令牌。
56 |
57 | #### 3.1 用户认证(登录)
58 |
59 | 请求示例 :
60 | (本例子中笔者对此模式的第一步登录做了改造,用户登录授权服务器需要也进行basic认证,目的是在一个认证授权中心里面,为了确认客户端和用户均有效且能够建立信任关系)
61 |
62 | 
63 |
64 | ```
65 | POST /login HTTP/1.1
66 | Host: localhost:8080
67 | Authorization: Basic QUJDOjEyMzQ1Ng==
68 | Content-Type: application/x-www-form-urlencoded
69 | Content-Length: 32
70 |
71 | username=ealenxie&password=admin
72 | ```
73 | 认证成功后,会在浏览器写入cookie内容。
74 |
75 |
76 | #### 3.2 获取授权码
77 |
78 | 请求示例 :
79 |
80 | ```
81 | GET /oauth/authorize?client_id=ABC&response_type=code&grant_type=authorization_code HTTP/1.1
82 | Host: localhost:8080
83 | Cookie: JSESSIONID=D329015F6B61C701BD69AE21CA5112C4
84 | ```
85 |
86 | 浏览器此接口调用成功后,会302到对应的redirect_uri,并且携带上code值。
87 |
88 |
89 | #### 3.3 授权码模式获取令牌
90 |
91 | 获取到code之后,再次调用获取令牌接口
92 |
93 | 
94 |
95 | ```
96 | POST /oauth/token HTTP/1.1
97 | Host: localhost:8080
98 | Authorization: Basic QUJDOjEyMzQ1Ng==
99 | Content-Type: application/x-www-form-urlencoded
100 | Content-Length: 90
101 |
102 | grant_type=authorization_code&redirect_uri=http://localhost:9528/code/redirect&code=3EZOug
103 | ```
104 |
105 | ### 4. 简化模式
106 |
107 | 此模式首先需要认证过的用户(见3.1 用户认证)直接进行授权,浏览器此接口调用授权接口成功后,会直接302到对应的redirect_uri,并且携带上token值,此时token以锚点的形式返回。
108 | 本例子中我在后台配置 redirect_uri 假设为 www.baidu.com 如下 :
109 | 
110 |
111 |
112 |
113 | 
114 |
115 | ### 5. 刷新令牌
116 |
117 | 本例中,设置的令牌有效期`access_token_validity`为7199秒,即两个小时。
118 | 刷新令牌的有效期`refresh_token_validity`为2592000秒,即30天。
119 | 当`access_token`过期且`refresh_token`未过期时,可以通过`refresh_token`进行刷新令牌,获取新的`access_token`和`refresh_token`
120 |
121 | 
122 |
123 | ```
124 | POST /oauth/token HTTP/1.1
125 | Host: localhost:8080
126 | Authorization: Basic QUJDOjEyMzQ1Ng==
127 | Content-Type: application/x-www-form-urlencoded
128 | Cookie: JSESSIONID=BC4B6A26370829BB3CAD6BED398F72C8
129 | Content-Length: 391
130 |
131 | grant_type=refresh_token&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9xxxx.....
132 |
133 | ```
134 |
135 | 此模式获取令牌接口 `grant_type`固定传值 refresh_token
136 |
137 | ### 6. 检查令牌是否有效
138 |
139 | 当需要进行确定令牌是否有效时,可以进行check_token
140 | 
141 |
142 | ```
143 | POST /oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiY2xvdWQtYXBpLXBsYXRmb3JtIl0sImV4cCI6MTYxMjM3OTkxMSwidXNlcl9uYW1lIjoiZWFsZW54aWUiLCJqdGkiOiJhZWVmMDhkZS02YTExLTQ3NDAtYTQzNS0wNTMyMThkYTMyYzkiLCJjbGllbnRfaWQiOiJBQkMiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXX0.NPTkpwwdnaKSiPzUgILnnhjawgAuw-ZZWk_4HbkfYzM HTTP/1.1
144 | Host: localhost:8080
145 | Authorization: Basic QUJDOjEyMzQ1Ng==
146 | Cookie: JSESSIONID=4838A3CFD6327A1644D1DAB0B095CC58
147 |
148 | ```
149 |
150 | ### 本例运行先决条件
151 |
152 | 1. 因为本例子中使用的数据库方式存储令牌,用户等等。需要准备spring_oauth2的相关数据表,执行本项目下的db脚本(里面配置了oauth2的基础表和客户端及用户账号信息)。
153 | 2. 运行项目
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | org.springframework.boot
8 | spring-boot-starter-parent
9 | 2.6.2
10 |
11 |
12 | com.github
13 | spring-oauth2-authenticator
14 | 1.0-SNAPSHOT
15 | SpringBoot整合spring-security-oauth2实现完整Oauth2
16 |
17 |
18 | org.projectlombok
19 | lombok
20 | true
21 |
22 |
23 | org.springframework.boot
24 | spring-boot-starter-web
25 |
26 |
27 | org.springframework.boot
28 | spring-boot-starter-security
29 |
30 |
31 | org.springframework.security
32 | spring-security-jwt
33 | 1.0.9.RELEASE
34 |
35 |
36 | org.springframework.boot
37 | spring-boot-starter-jdbc
38 |
39 |
40 | org.springframework.security.oauth
41 | spring-security-oauth2
42 | 2.3.6.RELEASE
43 |
44 |
45 | mysql
46 | mysql-connector-java
47 | runtime
48 |
49 |
50 | com.baomidou
51 | mybatis-plus-boot-starter
52 | 3.3.2
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/main/java/com/github/ealen/Oauth2AuthenticatorApplication.java:
--------------------------------------------------------------------------------
1 | package com.github.ealen;
2 |
3 | import org.mybatis.spring.annotation.MapperScan;
4 | import org.springframework.boot.SpringApplication;
5 | import org.springframework.boot.autoconfigure.SpringBootApplication;
6 |
7 | /**
8 | * @author EalenXie create on 2021/2/2 14:47
9 | */
10 | @MapperScan("com.github.ealen.domain.mapper")
11 | @SpringBootApplication
12 | public class Oauth2AuthenticatorApplication {
13 | public static void main(String[] args) {
14 | SpringApplication.run(Oauth2AuthenticatorApplication.class, args);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/com/github/ealen/domain/entity/OauthAccount.java:
--------------------------------------------------------------------------------
1 | package com.github.ealen.domain.entity;
2 |
3 | import com.baomidou.mybatisplus.annotation.IdType;
4 | import com.baomidou.mybatisplus.annotation.TableField;
5 | import com.baomidou.mybatisplus.annotation.TableId;
6 | import com.baomidou.mybatisplus.annotation.TableName;
7 | import lombok.Data;
8 |
9 | import java.io.Serializable;
10 | import java.util.Date;
11 |
12 | /**
13 | * @author EalenXie create on 2020/11/24 14:45
14 | * 自定义认证中心账号表
15 | */
16 | @Data
17 | @TableName("oauth_account")
18 | public class OauthAccount implements Serializable {
19 |
20 | private static final long serialVersionUID = 1L;
21 | /**
22 | * 用户ID
23 | */
24 | @TableId(type = IdType.AUTO)
25 | private Long id;
26 |
27 | /**
28 | * 客户端id
29 | */
30 | @TableField("client_id")
31 | private String clientId;
32 |
33 | /**
34 | * 账号名
35 | */
36 | @TableField("username")
37 | private String username;
38 |
39 | /**
40 | * 密码
41 | */
42 | @TableField("password")
43 | private String password;
44 |
45 | /**
46 | * 手机号
47 | */
48 | @TableField("mobile")
49 | private String mobile;
50 |
51 | /**
52 | * 邮箱
53 | */
54 | @TableField("email")
55 | private String email;
56 |
57 | /**
58 | * 是否可用
59 | */
60 | @TableField("enabled")
61 | private Boolean enabled;
62 |
63 | /**
64 | * 账号未过期
65 | */
66 | @TableField("account_non_expired")
67 | private Boolean accountNonExpired;
68 |
69 | /**
70 | * 账号未锁定
71 | */
72 | @TableField("account_non_locked")
73 | private Boolean accountNonLocked;
74 |
75 | /**
76 | * 密码未过期
77 | */
78 | @TableField("credentials_non_expired")
79 | private Boolean credentialsNonExpired;
80 |
81 | /**
82 | * 账号未删除(逻辑删除)
83 | */
84 | @TableField("account_non_deleted")
85 | private Boolean accountNonDeleted;
86 |
87 | /**
88 | * 创建时间
89 | */
90 | @TableField("created_time")
91 | private Date createdTime;
92 | /**
93 | * 更新时间
94 | */
95 | @TableField("updated_time")
96 | private Date updatedTime;
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/src/main/java/com/github/ealen/domain/mapper/OauthAccountMapper.java:
--------------------------------------------------------------------------------
1 | package com.github.ealen.domain.mapper;
2 |
3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4 | import com.github.ealen.domain.entity.OauthAccount;
5 | import org.apache.ibatis.annotations.Param;
6 |
7 | /**
8 | * @author EalenXie create on 2020/11/24 15:16
9 | */
10 | public interface OauthAccountMapper extends BaseMapper {
11 |
12 | /**
13 | * 获取客户端用户信息
14 | *
15 | * @param clientId 客户端Id
16 | * @param username 用户名
17 | * @return 用户对象
18 | */
19 | OauthAccount loadUserByUsername(@Param("clientId") String clientId, @Param("username") String username);
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/com/github/ealen/domain/vo/AccountInfo.java:
--------------------------------------------------------------------------------
1 | package com.github.ealen.domain.vo;
2 |
3 | import lombok.Data;
4 |
5 | import java.io.Serializable;
6 |
7 | /**
8 | * @author EalenXie create on 2020/11/24 19:16
9 | * 安全用户信息
10 | */
11 | @Data
12 | public class AccountInfo implements Serializable {
13 |
14 | private Long id;
15 |
16 | /**
17 | * 客户端id
18 | */
19 | private String clientId;
20 |
21 | /**
22 | * 账号名
23 | */
24 | private String username;
25 |
26 | /**
27 | * 手机号
28 | */
29 | private String mobile;
30 |
31 | /**
32 | * 邮箱
33 | */
34 | private String email;
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/com/github/ealen/domain/vo/AuthResp.java:
--------------------------------------------------------------------------------
1 | package com.github.ealen.domain.vo;
2 |
3 | /**
4 | * @author EalenXie create on 2021/2/1 10:46
5 | */
6 | public class AuthResp {
7 |
8 | private int status;
9 |
10 | private String message;
11 |
12 |
13 | public AuthResp(int status, String message) {
14 | this.status = status;
15 | this.message = message;
16 | }
17 |
18 | public AuthResp() {
19 | }
20 |
21 | public int getStatus() {
22 | return status;
23 | }
24 |
25 | public void setStatus(int status) {
26 | this.status = status;
27 | }
28 |
29 | public String getMessage() {
30 | return message;
31 | }
32 |
33 | public void setMessage(String message) {
34 | this.message = message;
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/com/github/ealen/infra/config/AuthorizationServerConfig.java:
--------------------------------------------------------------------------------
1 | package com.github.ealen.infra.config;
2 |
3 | import org.springframework.context.annotation.Configuration;
4 | import org.springframework.security.authentication.AuthenticationManager;
5 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
6 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
7 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
8 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
9 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
10 | import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
11 | import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
12 |
13 | import javax.annotation.Resource;
14 |
15 | /**
16 | * @author EalenXie create on 2020/11/3 11:34
17 | */
18 | @Configuration
19 | @EnableAuthorizationServer
20 | public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
21 |
22 |
23 | @Resource
24 | private AuthorizationServerTokenServices tokenServices;
25 | @Resource
26 | private AuthenticationManager authenticationManagerBean;
27 | @Resource
28 | private JdbcClientDetailsService jdbcClientDetailsService;
29 |
30 |
31 | @Override
32 | public void configure(AuthorizationServerSecurityConfigurer security) {
33 | security.tokenKeyAccess("permitAll()")
34 | .checkTokenAccess("isAuthenticated()")
35 | .allowFormAuthenticationForClients();
36 | }
37 |
38 | @Override
39 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
40 | clients.withClientDetails(jdbcClientDetailsService);
41 | }
42 |
43 | @Override
44 | public void configure(AuthorizationServerEndpointsConfigurer configurer) {
45 | configurer.tokenServices(tokenServices);
46 | configurer.authenticationManager(authenticationManagerBean);
47 | }
48 |
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/com/github/ealen/infra/config/OauthAccountUserDetails.java:
--------------------------------------------------------------------------------
1 | package com.github.ealen.infra.config;
2 |
3 | import com.github.ealen.domain.entity.OauthAccount;
4 | import org.springframework.security.core.GrantedAuthority;
5 | import org.springframework.security.core.userdetails.UserDetails;
6 |
7 | import java.util.Collection;
8 |
9 | /**
10 | * @author EalenXie create on 2020/11/24 15:09
11 | */
12 | public class OauthAccountUserDetails implements UserDetails {
13 |
14 |
15 | public OauthAccountUserDetails(OauthAccount oauthAccount, Collection extends GrantedAuthority> authorities) {
16 | this.oauthAccount = oauthAccount;
17 | this.authorities = authorities;
18 | }
19 |
20 | private final OauthAccount oauthAccount;
21 |
22 | private final Collection extends GrantedAuthority> authorities;
23 |
24 | public OauthAccount getOauthAccount() {
25 | return oauthAccount;
26 | }
27 |
28 | @Override
29 | public Collection extends GrantedAuthority> getAuthorities() {
30 | return authorities;
31 | }
32 |
33 | @Override
34 | public String getPassword() {
35 | return oauthAccount.getPassword();
36 | }
37 |
38 | @Override
39 | public String getUsername() {
40 | return oauthAccount.getUsername();
41 | }
42 |
43 | @Override
44 | public boolean isAccountNonExpired() {
45 | return oauthAccount.getAccountNonExpired();
46 | }
47 |
48 | @Override
49 | public boolean isAccountNonLocked() {
50 | return oauthAccount.getAccountNonLocked();
51 | }
52 |
53 | @Override
54 | public boolean isCredentialsNonExpired() {
55 | return oauthAccount.getCredentialsNonExpired();
56 | }
57 |
58 | @Override
59 | public boolean isEnabled() {
60 | return oauthAccount.getEnabled();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/com/github/ealen/infra/config/OauthAccountUserDetailsService.java:
--------------------------------------------------------------------------------
1 | package com.github.ealen.infra.config;
2 |
3 |
4 | import com.github.ealen.domain.entity.OauthAccount;
5 | import com.github.ealen.domain.mapper.OauthAccountMapper;
6 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
7 | import org.springframework.security.core.Authentication;
8 | import org.springframework.security.core.authority.SimpleGrantedAuthority;
9 | import org.springframework.security.core.context.SecurityContextHolder;
10 | import org.springframework.security.core.userdetails.User;
11 | import org.springframework.security.core.userdetails.UserDetails;
12 | import org.springframework.security.core.userdetails.UserDetailsService;
13 | import org.springframework.security.core.userdetails.UsernameNotFoundException;
14 | import org.springframework.security.crypto.password.PasswordEncoder;
15 | import org.springframework.security.oauth2.common.exceptions.BadClientCredentialsException;
16 | import org.springframework.security.oauth2.common.exceptions.UnauthorizedClientException;
17 | import org.springframework.security.oauth2.provider.ClientDetails;
18 | import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
19 | import org.springframework.security.web.authentication.www.BasicAuthenticationConverter;
20 | import org.springframework.stereotype.Service;
21 | import org.springframework.web.context.request.RequestContextHolder;
22 | import org.springframework.web.context.request.ServletRequestAttributes;
23 |
24 | import javax.annotation.Resource;
25 | import javax.servlet.http.HttpServletRequest;
26 | import java.util.ArrayList;
27 | import java.util.List;
28 |
29 | /**
30 | * @author EalenXie create on 2020/11/24 15:15
31 | */
32 | @Service
33 | public class OauthAccountUserDetailsService implements UserDetailsService {
34 |
35 | @Resource
36 | private OauthAccountMapper oauthAccountMapper;
37 |
38 | private final BasicAuthenticationConverter authenticationConverter = new BasicAuthenticationConverter();
39 | @Resource
40 | private JdbcClientDetailsService jdbcClientDetailsService;
41 | @Resource
42 | private PasswordEncoder passwordEncoder;
43 |
44 | @Override
45 | public UserDetails loadUserByUsername(String username) {
46 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
47 | String clientId;
48 | if (authentication != null) {
49 | Object principal = authentication.getPrincipal();
50 | if (principal instanceof User) {
51 | User clientUser = (User) principal;
52 | clientId = clientUser.getUsername();
53 | } else if (principal instanceof OauthAccountUserDetails) {
54 | getClientIdByRequest();
55 | return (OauthAccountUserDetails) principal;
56 | } else {
57 | throw new UnsupportedOperationException();
58 | }
59 | } else {
60 | clientId = getClientIdByRequest();
61 | }
62 | // 获取用户
63 | OauthAccount account = oauthAccountMapper.loadUserByUsername(clientId, username);
64 | // 用户不存在
65 | if (account == null || !account.getAccountNonDeleted()) {
66 | throw new UsernameNotFoundException("user not found");
67 | }
68 | // 授权
69 | List authorities = new ArrayList<>();
70 | return new OauthAccountUserDetails(account, authorities);
71 | }
72 |
73 |
74 | /**
75 | * 从httpRequest中获取并验证客户端信息
76 | */
77 | public String getClientIdByRequest() {
78 | ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
79 | if (attributes == null) throw new UnsupportedOperationException();
80 | HttpServletRequest request = attributes.getRequest();
81 | UsernamePasswordAuthenticationToken client = authenticationConverter.convert(request);
82 | if (client == null) {
83 | throw new UnauthorizedClientException("unauthorized client");
84 | }
85 | ClientDetails clientDetails = jdbcClientDetailsService.loadClientByClientId(client.getName());
86 | if (!passwordEncoder.matches((String) client.getCredentials(), clientDetails.getClientSecret())) {
87 | throw new BadClientCredentialsException();
88 | }
89 | return clientDetails.getClientId();
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/src/main/java/com/github/ealen/infra/config/OauthClientAccessTokenConfig.java:
--------------------------------------------------------------------------------
1 | package com.github.ealen.infra.config;
2 |
3 | import com.github.ealen.domain.vo.AccountInfo;
4 | import org.springframework.beans.BeanUtils;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.context.annotation.Configuration;
7 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
8 | import org.springframework.security.core.Authentication;
9 | import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
10 | import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
11 | import org.springframework.security.oauth2.provider.token.*;
12 | import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
13 | import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
14 |
15 | import javax.annotation.Resource;
16 | import javax.sql.DataSource;
17 | import java.util.Arrays;
18 | import java.util.HashMap;
19 | import java.util.Map;
20 |
21 | /**
22 | * @author EalenXie create on 2020/11/3 11:46
23 | */
24 | @Configuration
25 | public class OauthClientAccessTokenConfig {
26 |
27 |
28 | /**
29 | * 前面的 jwt key 我这里写死为 5371f568a45e5ab1f442c38e0932aef24447139b
30 | */
31 | private static final String SIGNING_KEY = "5371f568a45e5ab1f442c38e0932aef24447139b";
32 |
33 | @Resource
34 | private DataSource dataSource;
35 |
36 | /**
37 | * 声明 ClientDetails实现
38 | *
39 | * @return ClientDetailsService
40 | */
41 | @Bean
42 | public JdbcClientDetailsService jdbcClientDetailsService() {
43 | return new JdbcClientDetailsService(dataSource);
44 | }
45 |
46 |
47 |
48 | /**
49 | * 配置TokenStore token持久化
50 | */
51 | @Bean
52 | public TokenStore tokenStore() {
53 | return new JdbcTokenStore(dataSource);
54 | }
55 | /**
56 | * tokenService 配置
57 | */
58 | @Bean(name = "tokenServices")
59 | public AuthorizationServerTokenServices tokenServices() {
60 | DefaultTokenServices tokenServices = new DefaultTokenServices();
61 | tokenServices.setClientDetailsService(jdbcClientDetailsService());
62 | // 允许支持refreshToken
63 | tokenServices.setSupportRefreshToken(true);
64 | // refreshToken 不重用策略
65 | tokenServices.setReuseRefreshToken(false);
66 | //设置Token存储方式
67 | tokenServices.setTokenStore(tokenStore());
68 | tokenServices.setTokenEnhancer(tokenEnhancerChain());
69 | return tokenServices;
70 | }
71 |
72 |
73 |
74 |
75 | /**
76 | * 自定义TokenEnhancerChain 由多个TokenEnhancer组成
77 | */
78 | @Bean
79 | public TokenEnhancerChain tokenEnhancerChain() {
80 | TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
81 | tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter(), additionalInformationTokenEnhancer()));
82 | return tokenEnhancerChain;
83 | }
84 |
85 | /**
86 | * JWT 转换器
87 | */
88 | @Bean
89 | JwtAccessTokenConverter jwtAccessTokenConverter() {
90 | JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
91 | converter.setSigningKey(SIGNING_KEY);
92 | return converter;
93 | }
94 |
95 | /**
96 | * token 额外自定义信息 此例获取用户信息
97 | */
98 | @Bean
99 | public TokenEnhancer additionalInformationTokenEnhancer() {
100 | return (accessToken, authentication) -> {
101 | Map information = new HashMap<>(8);
102 | Authentication userAuthentication = authentication.getUserAuthentication();
103 | if (userAuthentication instanceof UsernamePasswordAuthenticationToken) {
104 | UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) userAuthentication;
105 | Object principal = token.getPrincipal();
106 | if (principal instanceof OauthAccountUserDetails) {
107 | OauthAccountUserDetails userDetails = (OauthAccountUserDetails) token.getPrincipal();
108 | AccountInfo accountInfo = new AccountInfo();
109 | BeanUtils.copyProperties(userDetails.getOauthAccount(), accountInfo);
110 | information.put("account_info", accountInfo);
111 | ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(information);
112 | }
113 | }
114 | return accessToken;
115 | };
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/main/java/com/github/ealen/infra/config/WebSecurityConfig.java:
--------------------------------------------------------------------------------
1 | package com.github.ealen.infra.config;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 | import com.github.ealen.domain.vo.AuthResp;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.context.annotation.Configuration;
7 | import org.springframework.context.annotation.Lazy;
8 | import org.springframework.http.HttpMethod;
9 | import org.springframework.http.HttpStatus;
10 | import org.springframework.http.MediaType;
11 | import org.springframework.security.authentication.AuthenticationManager;
12 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
13 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
14 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
15 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
16 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
17 | import org.springframework.security.crypto.password.PasswordEncoder;
18 | import org.springframework.security.web.authentication.AuthenticationFailureHandler;
19 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
20 | import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
21 | import org.springframework.web.cors.CorsUtils;
22 |
23 | import javax.annotation.Resource;
24 |
25 |
26 | /**
27 | * @author EalenXie create on 2020/11/3 13:00
28 | */
29 | @Configuration
30 | @EnableWebSecurity
31 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
32 |
33 | @Lazy
34 | @Resource
35 | private OauthAccountUserDetailsService oauthAccountUserDetailsService;
36 |
37 |
38 | private final ObjectMapper objectMapper = new ObjectMapper();
39 |
40 | /**
41 | * 这些接口 对于认证中心来说无需授权
42 | */
43 | protected static final String[] PERMIT_ALL_URL = {"/oauth/**", "/user/**", "/actuator/**", "/error", "/open/api"};
44 |
45 | @Override
46 | protected void configure(HttpSecurity http) throws Exception {
47 | http
48 | .cors()
49 | .and().csrf().disable()
50 | .authorizeRequests()
51 | //处理跨域请求中的Preflight请求
52 | .antMatchers(HttpMethod.OPTIONS).permitAll()
53 | .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
54 | .antMatchers(PERMIT_ALL_URL)
55 | .permitAll()
56 | .and()
57 | .formLogin()
58 | .loginProcessingUrl("/login")
59 | .usernameParameter("username")
60 | .passwordParameter("password")
61 | .successHandler(authenticationSuccessHandler())
62 | .failureHandler(authenticationFailureHandler())
63 | .and().logout()
64 | .logoutSuccessHandler(logoutSuccessHandler())
65 | .deleteCookies("JSESSIONID")
66 | .and().httpBasic();
67 | }
68 |
69 | @Bean
70 | public PasswordEncoder passwordEncoder() {
71 | return new BCryptPasswordEncoder();
72 | }
73 |
74 | /**
75 | * 登录成功处理器
76 | */
77 | @Bean
78 | public AuthenticationSuccessHandler authenticationSuccessHandler() {
79 | return (httpServletRequest, httpServletResponse, authentication) -> {
80 | httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
81 | AuthResp resp = new AuthResp(HttpStatus.OK.value(), "login success");
82 | httpServletResponse.getWriter().write(objectMapper.writeValueAsString(resp));
83 | };
84 | }
85 |
86 | /**
87 | * 登出成功处理器
88 | */
89 | @Bean
90 | public LogoutSuccessHandler logoutSuccessHandler() {
91 | return (httpServletRequest, httpServletResponse, authentication) -> {
92 | httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
93 | AuthResp resp = new AuthResp(HttpStatus.OK.value(), "logout success");
94 | httpServletResponse.getWriter().write(objectMapper.writeValueAsString(resp));
95 | };
96 | }
97 |
98 | /**
99 | * 常规登录失败处理器
100 | */
101 | @Bean
102 | public AuthenticationFailureHandler authenticationFailureHandler() {
103 | return (httpServletRequest, httpServletResponse, e) -> {
104 | httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
105 | httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
106 | AuthResp resp = new AuthResp(HttpStatus.UNAUTHORIZED.value(), e.getMessage());
107 | httpServletResponse.getWriter().write(objectMapper.writeValueAsString(resp));
108 | };
109 | }
110 |
111 | @Override
112 | protected void configure(AuthenticationManagerBuilder auth) throws Exception {
113 | auth.userDetailsService(oauthAccountUserDetailsService).passwordEncoder(passwordEncoder());
114 | auth.eraseCredentials(true);
115 | }
116 |
117 | @Bean
118 | @Override
119 | public AuthenticationManager authenticationManagerBean() throws Exception {
120 | return super.authenticationManagerBean();
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | application:
3 | name: oauth2-authenticator
4 | datasource:
5 | url: jdbc:mysql://localhost:3306/authorization_center?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
6 | username: root
7 | password: admin
8 | driver-class-name: com.mysql.cj.jdbc.Driver
9 |
10 |
11 | mybatis-plus:
12 | # 扫码 *Mapper.xml 路径
13 | mapper-locations: classpath:/mapper/**.xml
14 |
15 |
--------------------------------------------------------------------------------
/src/main/resources/db/data.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO `oauth_account`(id, client_id, username, password, mobile, email, enabled, account_non_expired,
2 | credentials_non_expired, account_non_locked, account_non_deleted)
3 | VALUES (1, 'ABC', 'ealenxie', '$2a$10$IzjmkjegAMXtycRnGyBZl.ZMwNxoUhCCCn8/lwlLswdMQ6TcvU3P2', '1232378743',
4 | 'abc@123.com', 1, 1, 1, 1, 1);
5 | INSERT INTO `oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`,
6 | `web_server_redirect_uri`, `authorities`, `access_token_validity`,
7 | `refresh_token_validity`, `additional_information`, `autoapprove`)
8 | VALUES ('ABC', 'demo-app', '$2a$10$LaY9MNGFaInbMTx1nhaVXuGwyqMmNExCYoGZK/FJL2G91SIfVnXp2', 'read,write',
9 | 'client_credentials,authorization_code,password,refresh_token,implicit', 'http://www.baidu.com', 'user', 7199,
10 | 2592000, NULL, 'true');
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/main/resources/db/schema.sql:
--------------------------------------------------------------------------------
1 | SET NAMES utf8mb4;
2 | SET
3 | FOREIGN_KEY_CHECKS = 0;
4 |
5 | /* OAUTH2.0 系统表 */
6 | drop table if exists oauth_access_token;
7 |
8 | drop table if exists oauth_approvals;
9 |
10 | drop table if exists oauth_client_details;
11 |
12 | drop table if exists oauth_client_token;
13 |
14 | drop table if exists oauth_code;
15 |
16 | drop table if exists oauth_refresh_token;
17 |
18 | /*==============================================================*/
19 | /* Table: oauth_access_token */
20 | /*==============================================================*/
21 | create table oauth_access_token
22 | (
23 | token_id varchar(255),
24 | token blob,
25 | authentication_id varchar(255) not null,
26 | user_name varchar(255),
27 | client_id varchar(255),
28 | authentication blob,
29 | refresh_token varchar(255),
30 | primary key (authentication_id)
31 | ) ENGINE = InnoDB;
32 |
33 | /*==============================================================*/
34 | /* Table: oauth_approvals */
35 | /*==============================================================*/
36 | create table oauth_approvals
37 | (
38 | userId varchar(255),
39 | clientId varchar(255),
40 | scope varchar(255),
41 | status varchar(10),
42 | expiresAt TIMESTAMP,
43 | lastModifiedAt TIMESTAMP
44 | ) ENGINE = InnoDB;
45 |
46 | /*==============================================================*/
47 | /* Table: oauth_client_details */
48 | /*==============================================================*/
49 | create table oauth_client_details
50 | (
51 | client_id varchar(255) not null,
52 | resource_ids varchar(255),
53 | client_secret varchar(255),
54 | scope varchar(255),
55 | authorized_grant_types varchar(255),
56 | web_server_redirect_uri varchar(255),
57 | authorities varchar(255),
58 | access_token_validity INTEGER,
59 | refresh_token_validity INTEGER,
60 | additional_information varchar(4096),
61 | autoapprove varchar(255),
62 | primary key (client_id)
63 | ) ENGINE = InnoDB;
64 |
65 | /*==============================================================*/
66 | /* Table: oauth_client_token */
67 | /*==============================================================*/
68 | create table oauth_client_token
69 | (
70 | token_id varchar(255),
71 | token blob,
72 | authentication_id varchar(255) not null,
73 | user_name varchar(255),
74 | client_id varchar(255),
75 | primary key (authentication_id)
76 | ) ENGINE = InnoDB;
77 |
78 | /*==============================================================*/
79 | /* Table: oauth_code */
80 | /*==============================================================*/
81 | create table oauth_code
82 | (
83 | code varchar(255),
84 | authentication blob
85 | ) ENGINE = InnoDB;
86 |
87 | /*==============================================================*/
88 | /* Table: oauth_refresh_token */
89 | /*==============================================================*/
90 | create table oauth_refresh_token
91 | (
92 | token_id varchar(255),
93 | token blob,
94 | authentication blob
95 | ) ENGINE = InnoDB;
96 |
97 |
98 | -- 自定义认证中心账号表
99 | drop table if exists oauth_account;
100 |
101 | /*==============================================================*/
102 | /* Table: oauth_account */
103 | /*==============================================================*/
104 | create table oauth_account
105 | (
106 | id int(11) not null auto_increment comment '账号ID',
107 | client_id varchar(50) not null comment '客户端ID',
108 | username varchar(50) not null comment '用户名',
109 | password varchar(200) comment '密码',
110 | mobile varchar(13) comment '手机号',
111 | email varchar(100) comment '邮箱',
112 | enabled tinyint(1) comment '账号可用',
113 | account_non_expired tinyint(1) default 1 comment '账号未过期',
114 | credentials_non_expired tinyint(1) default 1 comment '密码未过期',
115 | account_non_locked tinyint(1) default 1 comment '账号未锁定',
116 | account_non_deleted tinyint(1) default 1 comment '账号未删除',
117 | created_time datetime default CURRENT_TIMESTAMP comment '创建时间',
118 | updated_time datetime default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP comment '更新时间',
119 | primary key (id)
120 | );
121 |
122 | alter table `oauth_account`
123 | add index `user_idx` (`client_id`, `username`, `password`) using btree;
124 | alter table oauth_account
125 | comment '自定义认证中心账号表';
126 |
127 |
--------------------------------------------------------------------------------
/src/main/resources/mapper/OauthAccountMapper.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 |
15 |
--------------------------------------------------------------------------------