由于项目要用到OAuth2.0授权,需要自己开发一个OAuth2.0授权服务器,在网上看到Java Oauth2.0授权用的比较多两个框架Spring Security和Apache Oltu,因为项目都是基于Spring的,所以决定使用Spring Security来做Oauth2.0.
在网上搜索了好多教程,看完了还是云里雾里,有些细节也没有讲明白,结合网上教程和自己的慢慢摸索,浪费了好多时间,所以准备写这个教程,希望大家能少走弯路.
项目框架
Maven
Spring Boot
Spring Security
Druid
MySql
目录结构
创建Oauth2.0需要创建三个相关的表,直接使用官方的SQL脚本即可生成(不要修改表名和字段名).
-- ----------------------------
-- 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;
pom.xml
spring-boot-starter-parent 版本不要使用2.0以上版本,否则AuthenticationManager会NullPointerException.(在这里卡了好久,目前发现的解决办法就是使用2.0以下的版本)
<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">
<modelVersion>4.0.0modelVersion>
<groupId>com.lanxiaotugroupId>
<artifactId>oauthartifactId>
<version>0.0.1-SNAPSHOTversion>
<packaging>jarpackaging>
<name>oauthname>
<description>Demo project for Spring Bootdescription>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>1.5.9.RELEASEversion>
<relativePath/>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>6.0.6version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.0.19version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.security.oauthgroupId>
<artifactId>spring-security-oauth2artifactId>
<version>2.0.14.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
application.properties
#DataSource
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/splus?serverTimezone=Asia/Shanghai&useSSL=false&useUnicode=true&characterEncoding=utf-8
spring.datasource.username = root
spring.datasource.password = root
初始化大小,最小,最大
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
# 配置获取连接等待超时的时间
spring.datasource.maxWait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
配置DataSource数据源
@Configuration
public class DruidDataSourceConfig {
@Value("${spring.datasource.url:#{null}}")
private String dbUrl;
@Value("${spring.datasource.username: #{null}}")
private String username;
@Value("${spring.datasource.password:#{null}}")
private String password;
@Value("${spring.datasource.driver-class-name:#{null}}")
private String driverClassName;
@Value("${spring.datasource.initialSize:#{null}}")
private Integer initialSize;
@Value("${spring.datasource.minIdle:#{null}}")
private Integer minIdle;
@Value("${spring.datasource.maxActive:#{null}}")
private Integer maxActive;
@Value("${spring.datasource.maxWait:#{null}}")
private Integer maxWait;
@Value("${spring.datasource.timeBetweenEvictionRunsMillis:#{null}}")
private Integer timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.minEvictableIdleTimeMillis:#{null}}")
private Integer minEvictableIdleTimeMillis;
@Value("${spring.datasource.validationQuery:#{null}}")
private String validationQuery;
@Value("${spring.datasource.testWhileIdle:#{null}}")
private Boolean testWhileIdle;
@Value("${spring.datasource.testOnBorrow:#{null}}")
private Boolean testOnBorrow;
@Value("${spring.datasource.testOnReturn:#{null}}")
private Boolean testOnReturn;
@Value("${spring.datasource.poolPreparedStatements:#{null}}")
private Boolean poolPreparedStatements;
@Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize:#{null}}")
private Integer maxPoolPreparedStatementPerConnectionSize;
@Bean
@Primary
public DataSource dataSource(){
DruidDataSource datasource = new DruidDataSource();
datasource.setUrl(this.dbUrl);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setDriverClassName(driverClassName);
//configuration
if(initialSize != null) {
datasource.setInitialSize(initialSize);
}
if(minIdle != null) {
datasource.setMinIdle(minIdle);
}
if(maxActive != null) {
datasource.setMaxActive(maxActive);
}
if(maxWait != null) {
datasource.setMaxWait(maxWait);
}
if(timeBetweenEvictionRunsMillis != null) {
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
}
if(minEvictableIdleTimeMillis != null) {
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
}
if(validationQuery!=null) {
datasource.setValidationQuery(validationQuery);
}
if(testWhileIdle != null) {
datasource.setTestWhileIdle(testWhileIdle);
}
if(testOnBorrow != null) {
datasource.setTestOnBorrow(testOnBorrow);
}
if(testOnReturn != null) {
datasource.setTestOnReturn(testOnReturn);
}
if(poolPreparedStatements != null) {
datasource.setPoolPreparedStatements(poolPreparedStatements);
}
if(maxPoolPreparedStatementPerConnectionSize != null) {
datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
}
return datasource;
}
}
配置AuthorizationServerConfiguration
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Bean // 声明TokenStore实现
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Bean // 声明 ClientDetails实现
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
@Autowired
private TokenStore tokenStore;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@Autowired
private ClientDetailsService clientDetails;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
endpoints.tokenStore(tokenStore());
endpoints.userDetailsService(userService);
endpoints.setClientDetailsService(clientDetails);
//配置TokenServices参数
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); // 1天
endpoints.tokenServices(tokenServices);
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setSupportRefreshToken(true);
tokenServices.setTokenStore(tokenStore);
return tokenServices;
}
}
配置ResourceServerConfiguration
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").authenticated()
.anyRequest().authenticated();
}
}
配置WebSecurityConfiguration
@Configuration
public class WebSecurityConfiguration extends GlobalAuthenticationConfigurerAdapter {
private final UserService userService;
@Autowired
public WebSecurityConfiguration(UserService userService) {
this.userService = userService;
}
@Override
public void init(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
}
User对象
需要实现Serializable接口
public class User implements Serializable{
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;
}
}
UserService继承UserDetailsService,并在UserServiceImpl实现loadUserByUsername
UserService
public interface UserService extends UserDetailsService{
}
UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
/*模拟数据库操作*/
User user = new User();
user.setUsername("10086");
user.setPassword("123456");
return new CustomUserDetails(user);
}
}
创建UserDetails对象
此处继承的是org.springframework.security.core.userdetails.User,不是自己定义的User对象
public class CustomUserDetails extends org.springframework.security.core.userdetails.User {
private User user;
public CustomUserDetails(User user) {
super(user.getUsername(), user.getPassword(), true, true, true, true, Collections.EMPTY_SET);
this.user = user;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
在Controller定义一个方法
@RestController
public class IndexController {
@GetMapping("/sayHello")
private String sayHello(){
System.out.println("Hello World");
return "Hello World";
}
}
测试结果
使用比较常用Postman模拟请求
第二步:获取授权
在oauth_client_details表添加一个客户端
获取access_token,访问 http://localhost:8080/oauth/token (localhost:8080请修改为自己的)
这里的username和password属于oauth_client_details表里的client_id和client_secret
这里的username和password属于User表里的用户名和密码
获取到access_token和refresh_token后再次请求sayHello接口
在请求头带上token,其中value的格式是 bearer + token