OAuth是一個關於受權(authorization)的開放網絡標準,在全世界獲得普遍應用,目前的版本是2.0版。本文重點講解Spring Boot項目對OAuth2進行的實現,若是你對OAuth2不是很瞭解,你能夠先理解 OAuth 2.0 - 阮一峯,這是一篇對於oauth2很好的科普文章。html
oauth2根據使用場景不一樣,分紅了4種模式java
在項目中咱們一般使用受權碼模式,也是四種模式中最複雜的,一般網站中常常出現的微博,qq第三方登陸,都會採用這個形式。git
Oauth2受權主要由兩部分組成:github
在實際項目中以上兩個服務能夠在一個服務器上,也能夠分開部署。下面結合spring boot來講明如何使用。web
以前的文章已經對 Spring Security 進行了講解,這一節對涉及到 Spring Security 的配置不詳細講解。若不瞭解 Spring Security 先移步到 Spring Boot Security 詳解。redis
客戶端信息能夠存儲在內存、redis和數據庫。在實際項目中一般使用redis和數據庫存儲。本文采用數據庫。Spring 0Auth2 己經設計好了數據庫的表,且不可變。表及字段說明參照:Oauth2數據庫表說明 。spring
建立0Auth2數據庫的腳本以下:sql
DROP TABLE IF EXISTS `clientdetails`;
DROP TABLE IF EXISTS `oauth_access_token`;
DROP TABLE IF EXISTS `oauth_approvals`;
DROP TABLE IF EXISTS `oauth_client_details`;
DROP TABLE IF EXISTS `oauth_client_token`;
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `clientdetails` (
`appId` varchar(128) NOT NULL,
`resourceIds` varchar(256) DEFAULT NULL,
`appSecret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`grantTypes` varchar(256) DEFAULT NULL,
`redirectUrl` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additionalInformation` varchar(4096) DEFAULT NULL,
`autoApproveScopes` varchar(256) DEFAULT NULL,
PRIMARY KEY (`appId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_access_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(128) NOT NULL,
`user_name` varchar(256) DEFAULT NULL,
`client_id` varchar(256) DEFAULT NULL,
`authentication` blob,
`refresh_token` varchar(256) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_approvals` (
`userId` varchar(256) DEFAULT NULL,
`clientId` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`status` varchar(10) DEFAULT NULL,
`expiresAt` datetime DEFAULT NULL,
`lastModifiedAt` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(128) NOT NULL,
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`authorized_grant_types` varchar(256) DEFAULT NULL,
`web_server_redirect_uri` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_client_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(128) NOT NULL,
`user_name` varchar(256) DEFAULT NULL,
`client_id` varchar(256) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(256) DEFAULT NULL,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
複製代碼
爲了測試方便,咱們先插入一條客戶端信息。shell
INSERT INTO `oauth_client_details` VALUES ('dev', '', 'dev', 'app', 'password,client_credentials,authorization_code,refresh_token', 'http://www.baidu.com', '', 3600, 3600, '{\"country\":\"CN\",\"country_code\":\"086\"}', 'false');
複製代碼
用戶、權限、角色用到的表以下:數據庫
DROP TABLE IF EXISTS `user`;
DROP TABLE IF EXISTS `role`;
DROP TABLE IF EXISTS `user_role`;
DROP TABLE IF EXISTS `role_permission`;
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `user` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `role` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `user_role` (
`user_id` bigint(11) NOT NULL,
`role_id` bigint(11) NOT NULL
);
CREATE TABLE `role_permission` (
`role_id` bigint(11) NOT NULL,
`permission_id` bigint(11) NOT NULL
);
CREATE TABLE `permission` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`url` varchar(255) NOT NULL,
`name` varchar(255) NOT NULL,
`description` varchar(255) NULL,
`pid` bigint(11) NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO user (id, username, password) VALUES (1,'user','e10adc3949ba59abbe56e057f20f883e');
INSERT INTO user (id, username , password) VALUES (2,'admin','e10adc3949ba59abbe56e057f20f883e');
INSERT INTO role (id, name) VALUES (1,'USER');
INSERT INTO role (id, name) VALUES (2,'ADMIN');
INSERT INTO permission (id, url, name, pid) VALUES (1,'/**','',0);
INSERT INTO permission (id, url, name, pid) VALUES (2,'/**','',0);
INSERT INTO user_role (user_id, role_id) VALUES (1, 1);
INSERT INTO user_role (user_id, role_id) VALUES (2, 2);
INSERT INTO role_permission (role_id, permission_id) VALUES (1, 1);
INSERT INTO role_permission (role_id, permission_id) VALUES (2, 2);
複製代碼
resources
|____templates
| |____login.html
| |____application.yml
java
|____com
| |____gf
| | |____SpringbootSecurityApplication.java
| | |____config
| | | |____SecurityConfig.java
| | | |____MyFilterSecurityInterceptor.java
| | | |____MyInvocationSecurityMetadataSourceService.java
| | | |____ResourceServerConfig.java
| | | |____WebResponseExceptionTranslateConfig.java
| | | |____AuthorizationServerConfiguration.java
| | | |____MyAccessDecisionManager.java
| | |____entity
| | | |____User.java
| | | |____RolePermisson.java
| | | |____Role.java
| | |____mapper
| | | |____PermissionMapper.java
| | | |____UserMapper.java
| | | |____RoleMapper.java
| | |____controller
| | | |____HelloController.java
| | | |____MainController.java
| | |____service
| | | |____MyUserDetailsService.java
複製代碼
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
複製代碼
支持password模式要配置AuthenticationManager
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//校驗用戶
auth.userDetailsService( userService ).passwordEncoder( new PasswordEncoder() {
//對密碼進行加密
@Override
public String encode(CharSequence charSequence) {
System.out.println(charSequence.toString());
return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
}
//對密碼進行判斷匹配
@Override
public boolean matches(CharSequence charSequence, String s) {
String encode = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
boolean res = s.equals( encode );
return res;
}
} );
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.requestMatchers()
.antMatchers("/oauth/**","/login","/login-error")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and()
.formLogin().loginPage( "/login" ).failureUrl( "/login-error" );
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception{
return super.authenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return Objects.equals(charSequence.toString(),s);
}
};
}
}
複製代碼
/** * 認證服務器配置 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
/** * 注入權限驗證控制器 來支持 password grant type */
@Autowired
private AuthenticationManager authenticationManager;
/** * 注入userDetailsService,開啓refresh_token須要用到 */
@Autowired
private MyUserDetailsService userDetailsService;
/** * 數據源 */
@Autowired
private DataSource dataSource;
/** * 設置保存token的方式,一共有五種,這裏採用數據庫的方式 */
@Autowired
private TokenStore tokenStore;
@Autowired
private WebResponseExceptionTranslator webResponseExceptionTranslator;
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore( dataSource );
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//開啓密碼受權類型
endpoints.authenticationManager(authenticationManager);
//配置token存儲方式
endpoints.tokenStore(tokenStore);
//自定義登陸或者鑑權失敗時的返回信息
endpoints.exceptionTranslator(webResponseExceptionTranslator);
//要使用refresh_token的話,須要額外配置userDetailsService
endpoints.userDetailsService( userDetailsService );
}
}
複製代碼
/** * 資源提供端的配置 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
/** * 這裏設置須要token驗證的url * 能夠在WebSecurityConfigurerAdapter中排除掉, * 對於相同的url,若是兩者都配置了驗證 * 則優先進入ResourceServerConfigurerAdapter,進行token驗證。而不會進行 * WebSecurityConfigurerAdapter 的 basic auth或表單認證。 */
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers().antMatchers("/hi")
.and()
.authorizeRequests()
.antMatchers("/hi").authenticated();
}
}
複製代碼
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
HttpServletResponse response = (HttpServletResponse)servletResponse;
HttpServletRequest request = (HttpServletRequest)servletRequest;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Allow-Headers", ":x-requested-with,content-type");
filterChain.doFilter(servletRequest,servletResponse);
if (!request.getRequestURI().equals("/oauth/token")) {
invoke(fi);
}
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//執行下一個攔截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}
複製代碼
關鍵代碼就是這些,其餘類代碼參照後面提供的源碼地址。
[ 密碼模式須要參數:username , password , grant_type , client_id , client_secret ]
請求token
curl -X POST -d "username=admin&password=123456&grant_type=password&client_id=dev&client_secret=dev" http://localhost:8080/oauth/token
複製代碼
返回
{
"access_token": "d94ec0aa-47ee-4578-b4a0-8cf47f0e8639",
"token_type": "bearer",
"refresh_token": "23503bc7-4494-4795-a047-98db75053374",
"expires_in": 3475,
"scope": "app"
}
複製代碼
不攜帶token訪問資源,
curl http://localhost:8080/hi\?name\=zhangsan
複製代碼
返回提示未受權
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
複製代碼
攜帶token訪問資源
curl http://localhost:8080/hi\?name\=zhangsan\&access_token\=164471f7-6fc6-4890-b5d2-eb43bda3328a
複製代碼
返回正確
hi , zhangsan
複製代碼
刷新token
curl -X POST -d 'grant_type=refresh_token&refresh_token=23503bc7-4494-4795-a047-98db75053374&client_id=dev&client_secret=dev' http://localhost:8080/oauth/token
複製代碼
返回
{
"access_token": "ef53eb01-eb9b-46d8-bd58-7a0f9f44e30b",
"token_type": "bearer",
"refresh_token": "23503bc7-4494-4795-a047-98db75053374",
"expires_in": 3599,
"scope": "app"
}
複製代碼
[ 客戶端模式須要參數:grant_type , client_id , client_secret ]
請求token
curl -X POST -d "grant_type=client_credentials&client_id=dev&client_secret=dev" http://localhost:8080/oauth/token
複製代碼
返回
{
"access_token": "a7be47b3-9dc8-473e-967a-c7267682dc66",
"token_type": "bearer",
"expires_in": 3564,
"scope": "app"
}
複製代碼
獲取code
瀏覽器中訪問以下地址:
http://localhost:8080/oauth/authorize?response_type=code&client_id=dev&redirect_uri=http://www.baidu.com
複製代碼
跳轉到登陸頁面,輸入帳號和密碼進行認證:
認證後會跳轉到受權確認頁面(oauth_client_details 表中 「autoapprove」 字段設置爲true 時,不會出受權確認頁面):
確認後,會跳轉到百度,而且地址欄中會帶上咱們想獲得的code參數:
經過code換token
curl -X POST -d "grant_type=authorization_code&code=qS03iu&client_id=dev&client_secret=dev&redirect_uri=http://www.baidu.com" http://localhost:8080/oauth/token
複製代碼
返回
{
"access_token": "90a246fa-a9ee-4117-8401-ca9c869c5be9",
"token_type": "bearer",
"refresh_token": "23503bc7-4494-4795-a047-98db75053374",
"expires_in": 3319,
"scope": "app"
}
複製代碼
stackoverflow.com/questions/2…