oauth 2.0密码模式框架搭建(java)
oauth 2.0密码模式框架搭建(java)
项目源码下载地址:http://www.jiajiajia.club/file/info/8GG7iM/109
一、什么是oauth协议
OAuth(开放授权)是一个开放标准。允许第三方网站在用户授权的前提下访问在用户在服务商那里存储的各种信息。而这种授权无需将用户提供用户名和密码提供给该第三方网站。OAuth允许用户提供一个令牌给第三方网站,一个令牌对应一个特定的第三方网站,同时该令牌只能在特定的时间内访问特定的资源。
二、oauth2.0密码模式的授权流程
密码模式相比较于授权码模式来说更为简单,单不是特别的安全,一般用于信任的第三方应用授权。流程如下图。
一般就分为两个步骤
- 第三方应用携带用户名和密码请求认证服务器,认证服务器判断用户信息无误后返回给第三方应用一个token。
- 第三方应用获取到token后就可以携带着token去请求资源服务器,当然资源服务器也会去认证服务器去判断你携带的token是否合法。如果合法,才会返回给你需要的资源 。
三、需要的基本pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.curise.microservice</groupId>
<version>1.0-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>
<artifactId>password_server</artifactId>
<description>OAuth2.0密码模式</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- for OAuth 2.0 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
注意springboot的版本,如果太高或太低,可能会导致oauth的jar包找不到。
四、yml配置文件
主要是配置数据源,和mybatis。因为通常情况下,为了完成测试直接把客户端信息,和用户信息存放在内存中,而本次实验将客户端信息和用户信息存放在了数据库中。所以需要为oauth提供数据源。
server:
port: 8080
servlet:
context-path: / #项目路径
spring:
datasource:
username: root
password: jiajia123
url: jdbc:mysql://localhost:3306/oauth?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.jdbc.Driver
mybatis:
mapper-locations: classpath:oauth2/mapper/*Mapper.xml
type-aliases-package: oauth2.entity
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #sql日志打印
call-setters-on-nulls: true #解决返回类型为Map的时候如果值为null将不会封装此字段
五、数据库基本配置
数据库中需要三张表,分别存放客户端信息,token等。sql脚本如下,我用的数据库是mysql5.7版本
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;
-- ----------------------------
-- 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;
-- ----------------------------
-- 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;
六、授权服务器的主要配置
package oauth2.config;
import javax.sql.DataSource;
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.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.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import oauth2.service.UserService;
/**
* 授权服务器
* @author 硅谷探秘者(jia)
*/
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
/**
* 数据源
*/
@Autowired
private DataSource dataSource;
@Autowired
private UserService userService;
/**
* 使用数据库的方式存储token,及每次产生的token都存放在数据库,每个用户只会产生一个
* @return
*/
@Bean
public TokenStore tokenStore() {
// return new InMemoryTokenStore(); //使用内存的方式储存token
return new JdbcTokenStore(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 用户认证
endpoints.authenticationManager(authenticationManager);
/**
* 从数据库中验证用户信息
*/
endpoints.userDetailsService(userService);
endpoints.tokenStore(tokenStore());
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
/**
* 从数据库中加载判断客户端信息
*/
clients.jdbc(dataSource);
}
}
可以看到代码中注入了数据源DataSource用于访问数据库。
clients.jdbc(dataSource);这段代码配置从数据库获取判断客户端信息。
endpoints.tokenStore(tokenStore());这是配置token的储存方式,InMemoryTokenStore()是将token储存在内存中,而JdbcTokenStore(dataSource)是将token储存在数据库中。
endpoints.userDetailsService(userService);这行代码是配置用户的认证方式,这里配置的意思是我们要自定义从数据库中获取用户信息。userDetailsService方法需要一个UserDetailsService对象,所以我们要定义一个类实现UserDetailsService接口。如下:
package oauth2.service;
import org.springframework.security.core.userdetails.UserDetailsService;
public interface UserService extends UserDetailsService{
}
package oauth2.service.impl;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import oauth2.entity.CustomUserDetails;
import oauth2.entity.User;
import oauth2.service.UserService;
@Service
public class UserServiceImpl implements UserService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
/**
* 这里的s就是username,根据username去数据库查询密码
*/
System.out.println(s);
/**
* 模拟从数据库中获取用户信息
*/
User user = new User();
user.setUsername(s);
user.setPassword("root");
return new CustomUserDetails(user);
}
}
代码中在UserServiceImpl类中实现了UserDetailsService中的loadUserByUsername方法。他有一个参数s代表的就是第三方用户需要认证的用户名。此方法还需要返回一个UserDetails类型的对象,所以在此还需要定义两个类。
自定义用户
import java.io.Serializable;
public class User implements Serializable{
private static final long serialVersionUID = 1L;
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
import java.util.Collections;
public class CustomUserDetails extends org.springframework.security.core.userdetails.User {
private static final long serialVersionUID = 1L;
private User user;
public CustomUserDetails(User user) {
super(user.getUsername(), user.getPassword(), true, true, true, true, Collections.emptySet());
this.user = user;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
到此授权服务器和自定义用户认证方法的配置基本完成。
七、资源服务器安全配置
主要配置需要鉴权的服务器资源。
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;
/***
* 资源服务器
* @author 硅谷探秘者(jia)
*/
@Configuration
@EnableResourceServer
public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter {
/**
* 安全认证 配置需要鉴权的资源
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.requestMatchers()
.antMatchers("/**");
}
}
在此配置的是"/**"拦截所有的请求。
再定义一个控制器用于测试
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import oauth2.entity.CustomUserDetails;
import oauth2.entity.User;
/**
* 测试controller
* @author 硅谷探秘者(jia)
*/
@RestController
public class UserController {
@GetMapping("/api/userInfo")
public ResponseEntity<User> getUserInfo(){
CustomUserDetails user = (CustomUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String email = user.getUsername() + "@qq.com";
User userInfo = new User();
userInfo.setUsername(user.getUsername());
return ResponseEntity.ok(userInfo);
}
}
上述所有配置完成以后,就可以启动项目测试了。
下面提供一些客户端调用的测试代码
八、客户端测试需要资源
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
九、主要测试代码
import java.io.IOException;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class TestMain {
public static void main(String[] args) throws HttpException, IOException {
/**
* 获取token
*/
ResInfo res = getToken("http://localhost:8080/oauth/token?grant_type=password&scope=read&username=root&password=root");
System.out.println(res.getAccess_token());
/**
* 获取用户信息
*/
System.out.println(getUserInfo("http://localhost:8080/api/userInfo",res.getAccess_token()));
}
public static ResInfo getToken(String urlParam) throws HttpException, IOException {
// 创建httpClient实例对象
HttpClient httpClient = new HttpClient();
// 设置httpClient连接主机服务器超时时间:15000毫秒
httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(15000);
//username:password--->访问的用户名,密码,并使用base64进行加密,将加密的字节信息转化为string类型,encoding--->token
String encoding = DatatypeConverter.printBase64Binary("app1:1234".getBytes("UTF-8"));
// 创建GET请求方法实例对象
PostMethod getMethod = new PostMethod(urlParam);
// 设置post请求超时时间
getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 60000);
getMethod.setRequestHeader("Authorization", "Basic " + encoding);
getMethod.addRequestHeader("Content-Type", "application/json");
httpClient.executeMethod(getMethod);
String result = getMethod.getResponseBodyAsString();
getMethod.releaseConnection();
ResInfo res=JSONObject.toJavaObject(JSON.parseObject(result),ResInfo.class);
return res;
}
public static String getUserInfo(String urlParam,String token) throws HttpException, IOException {
HttpClient httpClient = new HttpClient();
httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(15000);
GetMethod getMethod = new GetMethod(urlParam);
getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 60000);
getMethod.setRequestHeader("Authorization", "Bearer " + token);
getMethod.addRequestHeader("Content-Type", "application/json");
httpClient.executeMethod(getMethod);
String result = getMethod.getResponseBodyAsString();
getMethod.releaseConnection();
return result;
}
}
正常情况下getToken方法返回
{"access_token":"25afcb6c-64b9-4a22-b83b-40876170c6d0","token_type":"bearer","expires_in":38828,"scope":"read"}
代表成功返回了token,此时可以拿着token请求 /api/userInfo 接口了,正常情况下返回
{"username":"root","password":null}
在执行getToken方法的时候要特别注意请求头上的Authorization参数,它是 "Basic "+client_id+client_secret后经过base64加密后的字符串。在执行getUserInfo方法的时候也需要在请求头上携带Authorization参数,这个参数的形式是"Bearer "+token(token是在执行getToken方法是返回的)。
如果用户名或密码错误将会返回
{"error":"invalid_grant","error_description":"坏的凭证"}
如果返回
{"timestamp":1577154713720,"status":401,"error":"Unauthorized","message":"Invalid basic authentication token","path":"/oauth/token"}
代表你的客户端配置信息有误。或者数据库中不存在该客户端的信息。注意客户端对的信息都保存再oauth_client_details表中,此时应该检查请求头上的Authorization参数是否正确,或检查数据库中是否有正确的客户端的配置信息。
注意,在请求token的时候应采用post方式,如果使用get方式将会返回
{"error":"method_not_allowed","error_description":"Request method 'GET' not supported"}
错误信息。