├── .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 | ![](https://img2020.cnblogs.com/blog/994599/202102/994599-20210203140609030-750274907.png) 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 | ![](https://img2020.cnblogs.com/blog/994599/202102/994599-20210203140849090-463914185.png) 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 | ![](https://img2020.cnblogs.com/blog/994599/202102/994599-20210203141150447-1796077322.png) 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 | ![](https://img2020.cnblogs.com/blog/994599/202102/994599-20210203141532941-1192533333.png) 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 | ![](https://img2020.cnblogs.com/blog/994599/202102/994599-20210203152616881-566304748.png) 110 | 111 | 112 | 113 | ![](https://img2020.cnblogs.com/blog/994599/202102/994599-20210203145138530-258931100.png) 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 | ![](https://img2020.cnblogs.com/blog/994599/202102/994599-20210203151933958-120036858.png) 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 | ![](https://img2020.cnblogs.com/blog/994599/202102/994599-20210203152744359-1285977795.png) 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 authorities) { 16 | this.oauthAccount = oauthAccount; 17 | this.authorities = authorities; 18 | } 19 | 20 | private final OauthAccount oauthAccount; 21 | 22 | private final Collection authorities; 23 | 24 | public OauthAccount getOauthAccount() { 25 | return oauthAccount; 26 | } 27 | 28 | @Override 29 | public Collection 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 | --------------------------------------------------------------------------------