前言
由于项目要用到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以下的版本)
<?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"><modelVersion>4.0.0</modelVersion><groupId>com.lanxiaotu</groupId><artifactId>oauth</artifactId><version>0.0.1-SNAPSHOT</version><packaging>jar</packaging><name>oauth</name><description>Demo project for Spring Boot</description><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>1.5.9.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version></properties><dependencies><!--MySql驱动--><!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>6.0.6</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!--Druid数据源--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.0.19</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.security.oauth</groupId><artifactId>spring-security-oauth2</artifactId><version>2.0.14.RELEASE</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></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@Primarypublic DataSource dataSource(){DruidDataSource datasource = new DruidDataSource();datasource.setUrl(this.dbUrl);datasource.setUsername(username);datasource.setPassword(password);datasource.setDriverClassName(driverClassName);//configurationif(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 {
@Autowiredprivate DataSource dataSource;@Bean // 声明TokenStore实现public TokenStore tokenStore() {return new JdbcTokenStore(dataSource);}@Bean // 声明 ClientDetails实现public ClientDetailsService clientDetails() {return new JdbcClientDetailsService(dataSource);}@Autowiredprivate TokenStore tokenStore;@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate UserService userService;@Autowiredprivate ClientDetailsService clientDetails;@Overridepublic 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@Primarypublic DefaultTokenServices tokenServices() {DefaultTokenServices tokenServices = new DefaultTokenServices();tokenServices.setSupportRefreshToken(true); tokenServices.setTokenStore(tokenStore); return tokenServices;}
}
配置ResourceServerConfiguration
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/**").authenticated().anyRequest().authenticated();}}
配置WebSecurityConfiguration
@Configuration
public class WebSecurityConfiguration extends GlobalAuthenticationConfigurerAdapter {
private final UserService userService;@Autowiredpublic WebSecurityConfiguration(UserService userService) {this.userService = userService;}@Overridepublic 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 {
@Overridepublic 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模拟请求
第一步:直接访问sayHello接口,可以看到未授权
第二步:获取授权
在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
刷新token