相关代码:参考码云上的oauth2jdbc【仅供本人参考使用,不对外开放】
一、项目环境配置
1.1 、JDK8
1.2、Spring Boot: 2.3.0.RELEASE
1.3、spring-cloud.version:Hoxton.SR5
二、准备工作
1、pom.xml
1.1、thymeleaf页面模板依赖(可选)
<!-- thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
1.2、starter-web依赖
<!-- starter-web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
1.3、OAuth2依赖
<!--OAuth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
1.4、数据库连接依赖
<!-- mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
<scope>runtime</scope>
</dependency>
<!-- alibaba 数据连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.13</version>
</dependency>
1.5、lombok
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
1.6、单元测试依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
1.7、完整的pom.xml文件
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.iotspider</groupId>
<artifactId>oauth2jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>oauth2jdbc</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR5</spring-cloud.version>
</properties>
<dependencies>
<!-- thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- starter-web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--OAuth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!-- mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
<scope>runtime</scope>
</dependency>
<!-- alibaba 数据连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.13</version>
</dependency>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、application.properties
2.1 thymeleaf的配置
spring.thymeleaf.prefix = classpath:/templates/
#spring.thymeleaf.suffix = .html
spring.thymeleaf.encoding = UTF-8
spring.thymeleaf.servlet.content-type=text/html
spring.thymeleaf.cache = false
2.2 数据库连接的配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security_oauth2?useUnicode=true&characterEncoding=utf8&useSSL=false&useTimezone=true&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
2.3 MyBatis的配置
mybatis.mapper-locations=classpath:mybatis/*/*.xml
#将扫描到的mapper.xml显示在控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
2.4完整的application.properties文件
mybatis.mapper-locations=classpath:mapper/*/*.xml
#将扫描到的mapper.xml显示在控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/oauth2jdbc?useUnicode=true&characterEncoding=utf8&useSSL=false&useTimezone=true&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
spring.thymeleaf.prefix = classpath:/templates/
#spring.thymeleaf.suffix = .html
spring.thymeleaf.encoding = UTF-8
spring.thymeleaf.servlet.content-type=text/html
spring.thymeleaf.cache = false
#http://localhost:8080/oauth/authorize?response_type=code&client_id=client&scope=read&redirect_uri=http://localhost:8090/callback
#http://localhost:8080/oauth/token?grant_type=authorization_code&code=25R5tn&client_id=client&client_secret=secret&redirect_uri=http://localhost:8090/callback
三、代码
所涉及的代码结构:
3.1WebSecurityConfig
package cn.iotspider.oauth2jdbc.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
//对全部方法进行验证
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
/**
* 配置用户登录验证服务
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
public void configure(WebSecurity web) {
//"oauth/check_token"是校验token的地址
web.ignoring().antMatchers("/oauth/check_token");
}
/**
* 必须注入,验证密码需要使用
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3.2 Oauth2AuthenticationServer
package cn.iotspider.oauth2jdbc.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
@Configuration
@EnableAuthorizationServer
public class Oauth2AuthenticationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
public DataSource dataSource;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Bean
public TokenStore tokenStore() {
//token默认保存在内存中(也可以保存在数据库、Redis中)。
//如果保存在中间件(数据库、Redis),那么资源服务器与认证服务器可以不在同一个工程中。
//注意:如果不保存access_token,则没法通过access_token取得用户信息
return new JdbcTokenStore(dataSource);
}
@Bean
public ClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
/**
* 配置客户端
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}
/**
* 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore()).userDetailsService(userDetailsService)
//reuseRefreshTokens设置为false时,每次通过refresh_token获得access_token时,
// 也会刷新refresh_token;也就是说,会返回全新的access_token与refresh_token。
//默认值是true,只返回新的access_token,refresh_token不变。
.reuseRefreshTokens(true)
// 允许 GET、POST 请求获取 token,即访问端点:oauth/token
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
/*
* 默认获取token的路径是/oauth/token,通过pathMapping方法,可改变默认路径
* pathMapping用来配置端点URL链接,有两个参数,都将以 "/" 字符为开始的字符串
* defaultPath:这个端点URL的默认链接
* customPath:你要进行替代的URL链接
*/
endpoints.pathMapping("/oauth/token", "/oauth/token");
}
/**
* 配置资源服务器向认证服务器请求及验证token的规则:默认允许获取token,但是需要授权后才能获取到
*过来验令牌有效性的请求,不是谁都能验的,必须要是经过身份认证的。
* 所谓身份认证就是,必须携带clientId,clientSecret,否则随便一请求过来验token是不验的
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// security.tokenKeyAccess("permitAll()").checkTokenAccess(
// "isAuthenticated()");
//默认允许获取token,但是需要授权后才能获取到 ,所以可以去掉 tokenKeyAccess()
security.checkTokenAccess("isAuthenticated()");
//允许客户端使用表单方式发送请求token的认证(因为表单一般是POST请求,所以使用POST方式发送获取token,但必须携带clientId,clientSecret,否则随便一请求过来验token是不验的)
security.allowFormAuthenticationForClients();
}
}
3.3 UserDetailsServiceImpl
package cn.iotspider.oauth2jdbc.config;
import cn.iotspider.oauth2jdbc.entity.TbPermission;
import cn.iotspider.oauth2jdbc.entity.TbUser;
import cn.iotspider.oauth2jdbc.service.TbPermissionService;
import cn.iotspider.oauth2jdbc.service.TbUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private TbUserService tbUserService;
@Autowired
private TbPermissionService tbPermissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
TbUser user = tbUserService.queryByUserName(username);
if(user == null){
throw new UsernameNotFoundException("不存在该用户!");
}
List<GrantedAuthority> authorities = new ArrayList<>();
//获取用户权限
List<TbPermission> tbPermissionList = tbPermissionService.queryByUserId(user.getId());
for(TbPermission permission : tbPermissionList){
authorities.add(new SimpleGrantedAuthority(permission.getEname()));
}
//返回认证用户
return new User(username, user.getPassword(), authorities);
}
}
主要涉及的就是上面三个类,其他的请看码云上的代码。
四、数据库表
4.1 Oauth2认证相关的:
①、oauth_access_token (token存储表)
②、oauth_client_details(客户端存储表)
③、oauth_code(授权码表)
④、oauth_refresh_token(refresh_token表)
4.2 用户相关的
①、tb_user(用户表)
②、tb_role(角色表)
③、tb_permission(权限表)
④、tb_user_role(用户角色表)
⑤、tb_role_permission(角色权限表)
五、测试
5.1获取授权码:
http://localhost:8080/oauth/authorize?response_type=code&client_id=client&scope=read&redirect_uri=http://localhost:8090/callback
client_id、scope、redirect_uri必须和客户端存储表设置的一致。
因为我在oauth_client_details数据表的autoapprove字段设置了true,所以在输入密码登陆成功后就不会有选择同意或拒绝授权这一步操作,而是直接跳回redirect_uri设置的地址,同时返回了授权码
登陆成功后获取的授权码:
5.2获取token令牌
http://localhost:8080/oauth/token?grant_type=authorization_code&code=F4eRew&client_id=client&client_secret=secret&redirect_uri=http://localhost:8090/callback
grant_type、client_id、client_secret、redirect_uri必须和客户端存储表设置的一致。code是上一步请求到的。
因为我在Oauth2AuthenticationServer类设置了允许使用GET方式获取token,所以可以在浏览器端发起请求,否则只能使用POST方式请求token.
获取到的token:
可以看到返回的还有一个refresh_token,如果想设置还返回refresh_token的话,需要在oauth_client_details数据表中的authorized_grant_types同时加入refresh_token
5.3刷新token
当token过期后可以使用refresh_token来再次获取token
http://localhost:8080/oauth/token?grant_type=refresh_token&refresh_token=e4c9e3aa-fa7b-4728-a31c-4cb4f95019cc&client_id=client&client_secret=secret
下图是有关refresh_token的配置
六、作为SSO服务器
加入redis的依赖,主要是来存储session,这样服务器端才能实现单点登录的功能
6.1 POM中新增Redis依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--使用redis来管理session,达到共享session的目的-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
6.2 在application.properties配置文件中加入redis连接的配置
#spring.session.store-type=redis
## Redis数据库索引(默认为0)
spring.redis.database=0
## Redis服务器地址
spring.redis.host=127.0.0.1
## Redis服务器连接端口
spring.redis.port=6379
## Redis服务器连接密码(默认为空)
spring.redis.password=
## 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active= 8
## 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
## 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
## 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
## 连接超时时间(毫秒)
#spring.redis.timeout=3000