项目地址百度网盘:spring-security-oauth2_免费高速下载|百度网盘-分享无限制
父工程使用的是spring-boot 2.1.3RELEASE
父工程依赖如下:
<?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.funtl</groupId>
<artifactId>oauth</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>spring-security-oauth2-server</module>
<module>spring-security-oauth2-server-JDBC</module>
<module>spring-security-oauth2-server-RBAC</module>
<module>spring-security-oauth2-resource</module>
</modules>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/>
</parent>
<properties>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
<mysql.version>5.1.32</mysql.version>
<mapper.starter.version>2.0.3</mapper.starter.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Cloud -->
<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>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
子工程认证中心
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>
<artifactId>oauth</artifactId>
<groupId>com.funtl</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.funtl</groupId>
<artifactId>spring-security-oauth2-server-RBAC</artifactId>
<dependencies>
<!-- Spring Boot -->
<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>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${hikaricp.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<!-- 排除 tomcat-jdbc 以使用 HikariCP -->
<exclusion>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>${mapper.starter.version}</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-core</artifactId>
<version>1.0.4</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-extra</artifactId>
<version>1.0.4</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-base</artifactId>
<version>1.0.4</version>
</dependency>
</dependencies>
</project>
application.yml配置文件如下:
spring:
application:
name: oauth2-server
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.2.163:3306/oauth2?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
hikari:
minimum-idle: 5
idle-timeout: 600000
maximum-pool-size: 10
auto-commit: true
pool-name: MyHikariCP
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
redis:
host: 192.168.2.163
server:
port: 8080
mybatis:
type-aliases-package: oauth2.server.domain
mapper-locations: classpath:mapper/*.xml
启动类:
package oauth2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@MapperScan(basePackages = "oauth2.server.mapper")
public class RBACServerApplication {
public static void main(String[] args) {
SpringApplication.run(RBACServerApplication.class,args);
}
}
权限服务控制类:AuthorizationServerConfiguration
endpoints.tokenStore(new MyRedisTokenStoreService(redisConnectionFactory,null))设置的MyRedisTokenStoreService用刷新redis时限来实现用户不掉线;endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))需要前端请求RefreshToken来获取新的token实现不掉线
package oauth2.server.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import javax.sql.DataSource;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* 注入 AuthenticationManager 支持password grant_type
*/
@Autowired
private AuthenticationManager authenticationManagerBean;
/**
*Primary注解,去除系统自动配置的数据源
* @return
*/
@Primary
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource(){
// 配置数据源(注意,我使用的是 HikariCP 连接池),以上注解是指定数据源,否则会有冲突
return DataSourceBuilder.create().build();
}
@Bean
public TokenStore tokenStore(){
// 基于 JDBC 实现,令牌保存到数据
return new JdbcTokenStore(dataSource());
}
@Bean
public ClientDetailsService jdbcDetailsService(){
// 基于 JDBC 实现,需要事先在数据库配置客户端信息
return new JdbcClientDetailsService(dataSource());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
.passwordEncoder(passwordEncoder)
.tokenKeyAccess("permitAll()") //允许/oauth/token调用
.checkTokenAccess("isAuthenticated()"); //允许/oauth/check_token被调用
}
/**
* 认证授权
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 读取客户端配置
//告诉springoauth2使用上面定义的jdbcDetailsService存储
clients.withClientDetails(jdbcDetailsService())
;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 设置令牌
//告诉springoauth2使用上面定义的tokenStore存储到数据库
// endpoints.tokenStore(tokenStore())
//把token相关东西存入redis中,需要前端RefreshTokens来实现用户不掉线
// endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
//自定义RedisTokenStore,可以根据用户浏览刷新token时限
endpoints.tokenStore(new MyRedisTokenStoreService(redisConnectionFactory,null))
.authenticationManager(authenticationManagerBean);
//该字段设置设置refresh token是否重复使用,true:reuse;false:no reuse.
// .reuseRefreshTokens(false);
//配置TokenService参数
DefaultTokenServices tokenService = new DefaultTokenServices();
tokenService.setTokenStore(endpoints.getTokenStore());
tokenService.setSupportRefreshToken(true);
tokenService.setClientDetailsService(endpoints.getClientDetailsService());
tokenService.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenService.setAccessTokenValiditySeconds((int) TimeUnit.MINUTES.toSeconds(2)); //3分钟
//refresh token时长大于2被的access_token时长,当access_token过期,
// 前端发送refresh_token重新获取access_token和refresh_token,如此实现登录一直操作登录不过期
//https://www.code996.cn/post/2018/token-front/
tokenService.setRefreshTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(6)); //6分钟
//该字段设置设置refresh token是否重复使用,true:reuse;false:no reuse.
tokenService.setReuseRefreshToken(false);
endpoints.tokenServices(tokenService);
}
}
WebSecurityConfiguration类:
设置加密方式及制定自己实现的UserDetailsServiceImpl类来拓展权限
package oauth2.server.config;
import lombok.SneakyThrows;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.HttpSecurity;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.and()
.formLogin()
.loginPage("/login").failureForwardUrl("/login-error")
// .successForwardUrl("/index")
.permitAll();
}
@Bean
@Override
public UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}
@Bean
public BCryptPasswordEncoder passwordEncoder(){
// 设置默认的加密方式
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
/**
* 管理器
* 加载这个管理器,让其支持password模式授权
* @return
* @throws Exception
*/
@Bean
@Override
@SneakyThrows
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/oauth/check_token");
}
}
redis存储token实现类MyRedisTokenStoreService
package oauth2.server.config;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import java.util.Date;
/**
* 2种方式续期token,1:前端接收到后台返回401然后请求refresh_token重新获取token
* 2:刷新redis里的token时限
* access_token续期
* 自定义TokenStoreService
*/
public class MyRedisTokenStoreService extends RedisTokenStore {
private ClientDetailsService clientDetailsService;
public MyRedisTokenStoreService(RedisConnectionFactory connectionFactory, ClientDetailsService clientDetailsService) {
super(connectionFactory);
this.clientDetailsService = clientDetailsService;
}
@Override
public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
OAuth2Authentication result = readAuthentication(token.getValue());
if (result != null) {
// 如果token没有失效 更新AccessToken过期时间
DefaultOAuth2AccessToken oAuth2AccessToken = (DefaultOAuth2AccessToken) token;
//重新设置过期时间
int validitySeconds = getAccessTokenValiditySeconds(result.getOAuth2Request());
if (validitySeconds > 0) {
oAuth2AccessToken.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
}
//将重新设置过的过期时间重新存入redis, 此时会覆盖redis中原本的过期时间
storeAccessToken(token, result);
}
return result;
}
protected int getAccessTokenValiditySeconds(OAuth2Request clientAuth) {
if (clientDetailsService != null) {
ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());
Integer validity = client.getAccessTokenValiditySeconds();
if (validity != null) {
return validity;
}
}
// 2 分钟.
int accessTokenValiditySeconds = 60 * 2;
return accessTokenValiditySeconds;
}
}
UserDetailsServiceImpls实现类:通过查询用户权限并在ResourceServerConfiguration设置需要访问的权限
package oauth2.server.config;
import oauth2.server.domain.TbPermission;
import oauth2.server.domain.TbUser;
import oauth2.server.service.TbPermissionService;
import oauth2.server.service.TbUserService;
import org.assertj.core.util.Lists;
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 tbUser = tbUserService.getByUsername(username);
List<GrantedAuthority> grantedAuthorities = Lists.newArrayList();
if (tbUser != null) {
// 获取用户授权
List<TbPermission> tbPermissions = tbPermissionService.selectByUserId(tbUser.getId());
// 声明用户授权
for (TbPermission tbPermission : tbPermissions) {
if (tbPermission != null && tbPermission.getEnname() != null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(tbPermission.getEnname());
grantedAuthorities.add(grantedAuthority);
}
}
}
return new User(tbUser.getUsername(),tbUser.getPassword(),grantedAuthorities);
}
}
资源模块自定义权限MyAuthorityPermitImpl
接口:
import org.springframework.security.core.Authentication;
import javax.servlet.http.HttpServletRequest;
/**
* 自定义权限类
*/
public interface MyAuthorityPermit {
/**
* 权限校验逻辑方法 ,登录逻辑本质上处理不了
* 当用户有权限返回true,没权限返回false
* 当用户登录成功后,通过用户携带的权限集合,验证用户本次访问的路径是否有访问权限
* @param request 获取请求路径,请求参数,请求作用域
* @param authentication 用户登录后security维护的用户登录标记
* @return true/false。 access方法,参数如果是字符串"true"有权限,"false"无权限
*/
public boolean hasAuthority(HttpServletRequest request, Authentication authentication);
}
实现类:
import com.aadata.CAManage.authority.MyAuthorityPermit;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component("myAuthorityPermitImpl")
public class MyAuthorityPermitImpl implements MyAuthorityPermit {
/**
* 权限校验逻辑方法 ,登录逻辑本质上处理不了
* 当用户有权限返回true,没权限返回false
* 当用户登录成功后,通过用户携带的权限集合,验证用户本次访问的路径是否有访问权限
* @param request 获取请求路径,请求参数,请求作用域
* @param authentication 用户登录后security维护的用户登录标记
* @return true/false。 access方法,参数如果是字符串"true"有权限,"false"无权限
*/
@Override
public boolean hasAuthority(HttpServletRequest request, Authentication authentication) {
// // 请求中携带的角色和资源(权限),一般不用这种,一般使用url
// String[] roles = request.getParameterValues("roles");
// String[] authorities = request.getParameterValues("authorities");
// 请求url判断
String servletPath = request.getServletPath();
System.out.println("请求路径:"+servletPath);
System.out.println("权限集合:"+authentication.getAuthorities());
System.out.println("权限集合:"+authentication.getAuthorities());
System.out.println("身份信息:"+authentication.getPrincipal());
System.out.println("凭证信息:"+authentication.getCredentials()); // 一直为空
System.out.println("明细:"+authentication.getDetails());
System.out.println("是否登录:"+authentication.isAuthenticated());
// // 角色判断
// for(String role:roles){
// String tem = "ROLE_"+role; // 前缀拼接
// // 判断用户权限集合中,是否包含当前请求路径需要的权限
// if(authentication.getAuthorities().contains(new SimpleGrantedAuthority(tem))){
// return true;
// }
// }
// // 权限(资源)判断
// for(String authority:authorities){
// // 判断用户权限集合中,是否包含当前请求路径需要的权限
// if(authentication.getAuthorities().contains(new SimpleGrantedAuthority(authority))){
// return true;
// }
// }
// 请求url判断,自己组装最终的servletPath,只要和UserDetailsServiceImpl中设置的grantedAuthorities一样即可
if(authentication.getAuthorities().contains(new SimpleGrantedAuthority(servletPath))){
// 本测试例子不满足
return true;
}
// 无权限,返回false
return false;
}
}
资源模块ResourceServerConfiguration(在spring-security-oauth2-resource子模块里)类:
在使用SpringEL表达式(@myAuthorityPermitImpl.hasAuthority(request,authentication))时,请求会报错,无法找到Bean:myAuthorityPermitImpl,所以引OAuth2WebSecurityExpressionHandler;具体原因请参考:
https://github.com/spring-projects/spring-security-oauth/issues/730#issuecomment-219480394
package com.oauth2.resource.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// .antMatchers("/").hasAuthority("SystemContent")
// .antMatchers("/view/**").hasAuthority("SystemContentView")
// .antMatchers("/insert/**").hasAuthority("SystemContentInsert")
// .antMatchers("/update/**").hasAuthority("SystemContentUpdate")
// .antMatchers("/delete/**").hasAuthority("SystemContentDelete")
.antMatchers("/**").access("@myAuthorityPermitImpl.hasAuthority(request,authentication)")
;
}
@Autowired
private OAuth2WebSecurityExpressionHandler expressionHandler;
@Bean
public OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler(ApplicationContext applicationContext) {
OAuth2WebSecurityExpressionHandler expressionHandler = new OAuth2WebSecurityExpressionHandler();
expressionHandler.setApplicationContext(applicationContext);
return expressionHandler;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
// super.configure(resources);
// resources.authenticationEntryPoint(new LLGAuthenticationEntryPoint());
// resources.accessDeniedHandler(customAccessDeniedHandler);
resources.expressionHandler(expressionHandler);
}
}
postman测试:
Authorization设置
如果不设置上面一步Authorization设置,在这边设置
请求参数设置及获取的token
使用access_token获取资源
如果配置refresh_token,也可以用refresh_token进行token的续约,postman请求如下