Spring Security
理论
认证
认证是指判断用户的身份。比如用户名密码登录,二维码登录,指纹认证等。。。
会话
为了避免用户每次都需要认证,用户的信息保存在会话中。常用的有基于session 的认证方式,
基于token的认证方式。
授权
授权是为了限制用户对资源的使用。授权需要用户先认证通过后再进行授权,用户拥有资源的访问则正常访问,没有权限则拒绝访问。
授权的数据模型
用户对哪些资源进行哪些操作。
who -> what -> how
资源:资源分为两类。
-
功能资源:系统中的菜单,按钮,代码方法等。对于web而言,每个功能资源对于一个URL
-
数据资源:数据资源由资源类型和资源实例组成。
- 资源类型:一张表就是一个资源类型
- 资源实例:表中的具体某一行数据为资源实例
授权我们至少需要创建5张表:
用户
角色
权限
用户角色关系表
角色权限关系表
其中角色只是为了方便给用户授权。
最终目的是给每个用户赋予权限。
6表则是如下关系:
用户
角色
资源
权限
用户角色关系表
角色权限关系表
RBAC
-
基于角色的访问(需要修改代码)
-
基于资源的访问(推荐)
Spring Boot 中使用
Spring Security 认证
SpringSecurity默认提供认证页面
maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
认证配置
- 定义用户信息服务
@Bean
public UserDetailsService userDetailsService(){
// 基于内存的认证
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("ADMIN").build());
manager.createUser(User.withUsername("lizi").password("456").authorities("USER").build());
return manager;
}
- 密码编码器
@Bean
public PasswordEncoder passwordEncoder(){
// 无加密(通常不用这个)
// return NoOpPasswordEncoder.getInstance();
// BCryp加密
return new BCryptPasswordEncoder();
}
- 安全拦截机制(相当于拦截器)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 对请求进行验证
.antMatchers("/role/**").permitAll() // 开放授权,不拦截
.antMatchers("/admin/**").hasAuthority("ADMIN") // 必须有ADMIN权限
.antMatchers("/user/**").hasAnyAuthority("USER", "ADMIN") //有任意一种权限 (当然这里也可以使用角色控制资源访问但是不推荐)
.anyRequest() //任意请求(这里主要指方法)
.authenticated() //// 需要身份认证
.and() //表示一个配置的结束
.formLogin().permitAll() //开启SpringSecurity内置的表单登录,会提供一个/login接口
.and()
.logout().permitAll() //开启SpringSecurity内置的退出登录,会为我们提供一个/logout接口
.and()
.csrf().disable(); //关闭csrf跨站伪造请求
}
认证流程
修改为数据库认证配置
之前的基于内存的认证 去除 即 去除之前的UserDetailsService 配置
@Service
public class UserDetailService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
/**
* 根据账号查询用户信息
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据获取账号信息
UserDto userDto = userRepository.getUserByUserName(username);
if (userDto == null) {
return null;
}
UserDetails userDetails = User.withUsername(userDto.getUsername())
.password(userDto.getPassword()).authorities("ADMIN").build();
return userDetails;
}
}
Security 授权流程
底层原理使用投票的形式。
Spring Security会话
获取用户身份
@GetMapping("/info")
public String info(){
String username = null;
// 当前认证通过的用户身份
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
Object principal = authentication.getPrincipal();
if (principal != null) {
username = "匿名";
}
if (principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
}
return username;
}
控制会话
我们可以通过一下选项准确控制会话何时创建以与spring security 的交互方式
机制 | 描述 |
---|---|
always | 如果没有session存在就创建一个 |
ifRequired | 如果需要就创建一个Session(默认) 登录时 |
never | Spring Security将不会创建session,但如果应用中其他地方创建了session,那么Spring Security 将会使用它 |
stateless | Spring Security 将绝对不会 创建session,也不使用session |
配置方式:
在Spring Security 的WebSecurityConfigurerAdapter
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
}
自定义页面
自定义login页面
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义登录
http.formLogin().loginPage("/login-view")
.loginProcessingUrl("/login")
.successForwardUrl("/login-success")
.permitAll();
}
自定义logout页面
@Override
protected void configure(HttpSecurity http) throws Exception {
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login-view?logout=true");
// 配置session
}
Spring Security 授权
- 建表SQL
CREATE DATABASE /*!32312 IF NOT EXISTS*/`security` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `security`;
/*Table structure for table `t_permission` */
DROP TABLE IF EXISTS `t_permission`;
CREATE TABLE `t_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`code` varchar(255) DEFAULT NULL,
`description` varchar(255) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*Table structure for table `t_role` */
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_name` varchar(255) DEFAULT NULL,
`description` varchar(255) DEFAULT NULL,
`insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`status` int(11) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*Table structure for table `t_role_permission` */
DROP TABLE IF EXISTS `t_role_permission`;
CREATE TABLE `t_role_permission` (
`role_id` bigint(20) NOT NULL,
`permission_id` bigint(20) NOT NULL,
PRIMARY KEY (`role_id`,`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*Table structure for table `t_user` */
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` char(64) NOT NULL,
`password` char(64) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`mobile` char(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*Table structure for table `t_user_role` */
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`user_id` bigint(20) NOT NULL,
`role_id` bigint(20) NOT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
- 获取权限SQL
SELECT * FROM t_permission WHERE id IN (
SELECT permission_id FROM t_role_permission WHERE role_id IN (
SELECT role_id FROM t_user_role WHERE user_id = (
SELECT id FROM t_user WHERE username = 'zhangsan'
)
)
)
# 或者使用下面的SQL
SELECT * FROM t_permission AS p,t_role_permission AS rp,t_role AS r
WHERE r.id=rp.role_id AND rp.permission_id=p.id AND r.id
IN
(SELECT r.id FROM t_user AS u,t_role AS r,t_user_role AS ur
WHERE u.username ='zhangsan' AND u.id=ur.user_id AND ur.role_id=r.id);
web授权(url 拦截授权)
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 对请求进行验证
.antMatchers("/role/**").permitAll() // 开放授权
.antMatchers("/admin/**").hasAuthority("ADMIN") // 必须有ADMIN权限
.antMatchers("/user/**").hasAnyAuthority("USER", "ADMIN") //有任意一种权限
.antMatchers("/xx/**").hasAnyRole("TK"); // 包含这个角色
}
HttpSecurity 常用方法及说明
方法 | 说明 |
---|---|
openidLogin() | 用于基于 OpenId 的验证 |
headers() | 将安全标头添加到响应 |
cors() | 配置跨域资源共享( CORS ) |
sessionManagement() | 允许配置会话管理 |
portMapper() | 允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443 |
jee() | 配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理 |
x509() | 配置基于x509的认证 |
rememberMe | 允许配置“记住我”的验证 |
authorizeRequests() | 允许基于使用HttpServletRequest限制访问 |
requestCache() | 允许配置请求缓存 |
exceptionHandling() | 允许配置错误处理 |
securityContext() | 在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将自动应用 |
servletApi() | 将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用 |
csrf() | 添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用 |
logout() | 添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success” |
anonymous() | 允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS” |
formLogin() | 指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面 |
oauth2Login() | 根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证 |
requiresChannel() | 配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射 |
httpBasic() | 配置 Http Basic 验证 |
addFilterAt() | 在指定的Filter类的位置添加过滤器 |
方法授权(方法拦截授权)
基于方法的授权本质上是使用AOP的方法进行授权。所以我们可以在方法之前授权或者在方法之后授权。
- @PreAuthorize(hasAuthority(权限名)) | @PreAuthorize(hasAnyAuthority(权限名...)) 方法之前
- @PostAuthorize(hasAuthority(权限名)) |@PostAuthorize(hasAnyAuthority(权限名..)) 方法之后
使用方式
配置注解
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
Spring Boot项目地址 https://github.com/yusonghu/spring-security-demo
Spring Cloud Security OAuth2
介绍
Spring Security OAuth2 是对OAuth2的一种实现,OAuth2的服务提供方包括两个服务:
- 授权服务(认证服务)
- 资源服务
使用Spring Security OAuth2的时候可以选择他们在同一个应用中实现,也可以选择建立使用同一个授权服务的多个资源服务。
授权服务
AuthorizationEndpoint
服务于认证请求:默认URL:/oauth/authorizeTokenEndpoint
服务于访问令牌请求:默认URL:/oauth/tokenOAuth2AuthenticationProcessingFilter
用来对请求给出的身份令牌解析鉴权
创建一个简单的微服务
目录模块如下图:
编写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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>spring-security-oauth2-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>spring-security-oauth2-uaa</module>
<module>spring-security-oauth2-order</module>
<module>spring-security-oauth2-gateway</module>
<module>spring-security-oauth2-discovery</module>
</modules>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<finalName>${project.name}</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
<plugins>
<!--<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
</configuration>
</plugin>
</plugins>
</build>
</project>
- 授权认证服务(uaa)
<?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>spring-security-oauth2-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-oauth2-uaa</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
- 订单服务(order)
<?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>spring-security-oauth2-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-oauth2-order</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
- 网关(gateway)
<?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>spring-security-oauth2-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-oauth2-gateway</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
- 服务发现(discovery)
<?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>spring-security-oauth2-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-oauth2-discovery</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
</project>
授权服务器配置
@Configuration
@EnableAuthorizationServer
public class Authorization extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
// 使用内存的方式
clients.inMemory() // 使用内存的方式
.withClient("phone") // 客户端id
.secret(new BCryptPasswordEncoder().encode("secret")) // 客户端密钥
.resourceIds("res") // 资源列表
.authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
.scopes("all") // 授权允许范围
.autoApprove(false) // false 跳转到授权页面 true 直接发令牌
.redirectUris("http://www.baidu.com"); //验证回调地址
}
}
管理令牌
// 令牌存储策略
// 注入bean
@Bean
public TokenStore tokenStore(){
// 基于内存的token
return new InMemoryTokenStore();
}
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
// 令牌访问服务
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setClientDetailsService(clientDetailsService);
defaultTokenServices.setSupportRefreshToken(true); // 是否产生刷新令牌
defaultTokenServices.setAccessTokenValiditySeconds(7200); // 令牌有效期默认两小时
defaultTokenServices.setRefreshTokenValiditySeconds(2592000); // 刷新令牌默认有效期3天
defaultTokenServices.setTokenStore(tokenStore); // 令牌存储策略
return defaultTokenServices;
}
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
// 令牌访问端点
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure(endpoints);
endpoints.authenticationManager(authenticationManager) // 密码模式
.authorizationCodeServices(authorizationCodeServices) // 授权码模式
.tokenServices(tokenServices()) // 令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST); // 允许POST请求
}
// 令牌访问安全策略
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure(security);
security.tokenKeyAccess("permitAll()") // /oauth/token_key 公开
.checkTokenAccess("permitAll()") // /oauth/check_token 公开
.allowFormAuthenticationForClients(); //允许表单提交
}
- Spring Security OAuth2 默认令牌端点
/oauth/authorize
授权端点/oauth/token
令牌端点/oauth/error
授权服务错误信息端点/oauth/confirm_access
用户确认授权提交端点/oauth/check_token
用于资源服务访问的令牌解析端点/oauth/token_key
提供公有密匙的端点,如果使用JWT的话
之后可以把之前spring boot 的授权给拷贝过来。
授权服务配置总结:授权服务配置分成三大块,可以关联记忆。
既然要完成认证,它首先得知道客户端信息从哪儿读取,因此要进行客户端详情配置。
既然要颁发token,那必须得定义token的相关endpoint,以及token如何存取,以及客户端支持哪些类型的
token。
既然暴露除了一些endpoint,那对这些endpoint可以定义一些安全上的约束等。
授权码模式
介绍
- 资源拥有者打开客户端,客户端要求资源拥有者授权,重定向到授权服务器。
/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
参数列表如下:client_id
:客户端准入标识response_type
:授权码模式固定为codescope
:客户端权限redirect_uri
:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。
- 浏览器出现向授权服务器授权页面,之后将用户同意授权
- 授权服务器将授权码(AuthorizationCode)转经浏览器发送给client(通过redirect_uri)。
- 客户端拿着授权码向授权服务器索要访问access_token,请求如下:
-
获取授权码url
/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
-
获取token(POST)
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://www.baidu.com
PS:授权码模式拿到的授权码获取一次token之后授权码失效。
简化模式
- 资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息
/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
参数列表如下:
-
client_id:客户端准入标识。
-
response_type:授权码模式固定为code。
-
scope:客户端权限。
-
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。
- 浏览器出现向授权服务器授权页面,之后将用户同意授权。
- 授权服务器将授权码(AuthorizationCode)转经浏览器发送给client(通过redirect_uri)。
- 客户端拿着授权码向授权服务器索要访问access_token
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://www.baidu.com
PS: 一般来说,简化模式用于没有服务端的第三方单页面应用,因为没有服务端,所以无法接收授权码。
密码模式
- 资源拥有者将用户名、密码发送给客户端
- 客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token)
uaa/oauth/token?%20client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123](http://uaa/oauth/token? client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123)
参数列表如下:
-
client_id:客户端准入标识。
-
client_secret:客户端秘钥。
-
grant_type:授权类型,填写password表示密码模式
-
username:资源拥有者用户名。
-
password:资源拥有者密码。
- 授权服务器将令牌(access_token)发送给client
PS:这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client,因此这就说明这种模式只能用于client是我 们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用。
客户端模式
- 客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)
- 确认客户端身份无误后,将令牌(access_token)发送给client
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
参数列表如下:
-
client_id:客户端准入标识。
-
client_secret:客户端秘钥。
-
grant_type:授权类型,填写client_credentials表示客户端模式
PS: 这种模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任,而client本身也是安全的。因
此这种模式一般用来提供给我们完全信任的服务器端服务。比如,合作方系统对接,拉取一组用户信息。
添加资源服务
资源服务配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
// 资源ID (这个得与授权服务得客户端得ResourceId 一致)
public static final String RESOURCE_ID = "res1";
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
resources.resourceId(RESOURCE_ID) //资源ID
.tokenServices(tokenService()) // 验证令牌服务
.stateless(true); // 无状态
}
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests().antMatchers("/**")
.access("#oauth2.hasAnyScope('all')") // 这个scope是授权服务器得授的授权范围
.and().csrf().disable() // 禁用csrf
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 不启用session
}
// 验证token
@Bean
public ResourceServerTokenServices tokenService() {
// 远程服务请求授权服务器校验token,必须指定校验token、client_id、client_secret
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
remoteTokenServices.setClientId("phone"); // client_id
remoteTokenServices.setClientSecret("secret"); // client_secret
return remoteTokenServices;
}
}
资源服务安全拦截
这个这个配置就是服务内部的权限管理
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.anyRequest().permitAll();
}
}
JWT 令牌
介绍
JSON Web Token(JWT) 是一个开放的行业标准,它定义了一种简洁的、自包含的协议格式,用于在通信双方传递JSON对象。
标准:https://tools.ietf.org/html/rfc7519
结构
- Header 头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)
例如:
{ "alg": "HS256", "typ": "JWT" }
- Payload 第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比 如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。
例如:
{ "sub": "1234567890", "name": "456", "admin": true }
- Signature 第三部分是签名,此部分用于防止jwt内容被篡改。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:签名所使用的密钥。
将授权服务器颁发JWT令牌
- 修改存储方式
private String SINGING_KEY = "SIGIN_UAA";
@Bean
public TokenStore tokenStore() {
// JWT 令牌存储方案
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SINGING_KEY); // 对称密钥,资源服务器使用该密钥来验证
return converter;
}
- 定义令牌服务
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
// 令牌访问服务
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setClientDetailsService(clientDetailsService);
defaultTokenServices.setSupportRefreshToken(true); // 是否产生刷新令牌
defaultTokenServices.setAccessTokenValiditySeconds(7200); // 令牌有效期默认两小时
defaultTokenServices.setRefreshTokenValiditySeconds(2592000); // 刷新令牌默认有效期3天
defaultTokenServices.setTokenStore(tokenStore); // 令牌存储策略
// 设置令牌增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);
return defaultTokenServices;
}
-
资源服务修改为本地校验
- 拷贝一份TokenConfig到资源服务中校验
@Configuration public class TokenConfig { private String SINGING_KEY = "SIGIN_UAA"; @Bean public TokenStore tokenStore() { // JWT 令牌存储方案 return new JwtTokenStore(accessTokenConverter()); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(SINGING_KEY); // 对称密钥,资源服务器使用该密钥来验证 return converter; } }
- 将远程校验设置校验服务为本地校验
@Autowired private TokenStore tokenStore; @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { super.configure(resources); resources.resourceId(RESOURCE_ID) //资源ID // .tokenServices(tokenService()) // 远程验证令牌服务 .tokenStore(tokenStore) // 本地验证令牌服务 .stateless(true); // 无状态 }
将客户端信息接入到数据库中
建表
- 接入客户端信息
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '客户端标 识',
`resource_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '接入资源列表',
`client_secret` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '客户端秘钥',
`scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`authorized_grant_types` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`web_server_redirect_uri` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`authorities` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` longtext CHARACTER SET utf8 COLLATE utf8_general_ci,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`archived` tinyint(4) DEFAULT NULL,
`trusted` tinyint(4) DEFAULT NULL,
`autoapprove` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='接入客户端信息';
/*Data for the table `oauth_client_details` */
insert into `oauth_client_details`(`client_id`,`resource_ids`,`client_secret`,`scope`,`authorized_grant_types`,`web_server_redirect_uri`,`authorities`,`access_token_validity`,`refresh_token_validity`,`additional_information`,`create_time`,`archived`,`trusted`,`autoapprove`) values ('pc','res2','$2a$10$NlBC84MVb7F95EXYTXwLneXgCca6/GipyWR5NHm8K0203bSQMLpvm','ROLE_API','client_credentials,password,authorization_code,implicit,refresh_token','http://www.baidu.com',NULL,31536000,2592000,NULL,'2020-07-31 16:18:54',0,0,'false'),('phone','res1','$2a$10$NlBC84MVb7F95EXYTXwLneXgCca6/GipyWR5NHm8K0203bSQMLpvm','ROLE_ADMIN,ROLE_USER,ROLE_API','client_credentials,password,authorization_code,implicit,refresh_token','http://www.baidu.com',NULL,7200,259200,NULL,'2020-07-31 16:18:47',0,0,'false');
- 存储授权码表
CREATE TABLE `oauth_code` (
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`authentication` blob,
KEY `code_index` (`code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
- 官方给出的表SQL
DROP TABLE IF EXISTS `clientdetails`;
CREATE TABLE `clientdetails` (
`appId` varchar(128) NOT NULL,
`resourceIds` varchar(256) DEFAULT NULL,
`appSecret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`grantTypes` varchar(256) DEFAULT NULL,
`redirectUrl` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additionalInformation` varchar(4096) DEFAULT NULL,
`autoApproveScopes` varchar(256) DEFAULT NULL,
PRIMARY KEY (`appId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `oauth_access_token` */
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(128) NOT NULL,
`user_name` varchar(256) DEFAULT NULL,
`client_id` varchar(256) DEFAULT NULL,
`authentication` blob,
`refresh_token` varchar(256) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `oauth_approvals` */
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (
`userId` varchar(256) DEFAULT NULL,
`clientId` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`status` varchar(10) DEFAULT NULL,
`expiresAt` timestamp NULL DEFAULT NULL,
`lastModifiedAt` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `oauth_client_details` */
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(128) NOT NULL,
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`authorized_grant_types` varchar(256) DEFAULT NULL,
`web_server_redirect_uri` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `oauth_client_token` */
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(128) NOT NULL,
`user_name` varchar(256) DEFAULT NULL,
`client_id` varchar(256) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `oauth_code` */
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(256) DEFAULT NULL,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `oauth_refresh_token` */
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `tb_permission` */
DROP TABLE IF EXISTS `tb_permission`;
CREATE TABLE `tb_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父权限',
`name` varchar(64) NOT NULL COMMENT '权限名称',
`enname` varchar(64) NOT NULL COMMENT '权限英文名称',
`url` varchar(255) NOT NULL COMMENT '授权路径',
`description` varchar(200) DEFAULT NULL COMMENT '备注',
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8 COMMENT='权限表';
/*Table structure for table `tb_role` */
DROP TABLE IF EXISTS `tb_role`;
CREATE TABLE `tb_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父角色',
`name` varchar(64) NOT NULL COMMENT '角色名称',
`enname` varchar(64) NOT NULL COMMENT '角色英文名称',
`description` varchar(200) DEFAULT NULL COMMENT '备注',
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='角色表';
/*Table structure for table `tb_role_permission` */
DROP TABLE IF EXISTS `tb_role_permission`;
CREATE TABLE `tb_role_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) NOT NULL COMMENT '角色 ID',
`permission_id` bigint(20) NOT NULL COMMENT '权限 ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8 COMMENT='角色权限表';
/*Table structure for table `tb_user` */
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(64) NOT NULL COMMENT '密码,加密存储',
`phone` varchar(20) DEFAULT NULL COMMENT '注册手机号',
`email` varchar(50) DEFAULT NULL COMMENT '注册邮箱',
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`) USING BTREE,
UNIQUE KEY `phone` (`phone`) USING BTREE,
UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户表';
/*Table structure for table `tb_user_role` */
DROP TABLE IF EXISTS `tb_user_role`;
CREATE TABLE `tb_user_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户 ID',
`role_id` bigint(20) NOT NULL COMMENT '角色 ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户角色表';
ClientDetailService和AuthorizationCodeServices从数据库中读取
// 将客户端信息从数据库中来
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
// 使用数据库的方式
clients.withClientDetails(clientDetailsService);
}
将授权码存入数据库
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource){
// 设置授权码模式的授权码如何存取
return new JdbcAuthorizationCodeServices(dataSource);
}
接入网关
网关资源服务配置
@Configuration
public class ResourceServerConfig {
// 资源ID (这个得与授权服务得客户端得ResourceId 一致)
public static final String RESOURCE_ID = "res1";
/**
* Uaa资源配置
*/
@Configuration
@EnableResourceServer
public class UaaServerConfig extends ResourceServerConfigurerAdapter{
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
resources.resourceId(RESOURCE_ID) //资源ID
.tokenStore(tokenStore) // 本地验证令牌服务
.stateless(true); // 无状态
}
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
// 将uaa全部放行
http.authorizeRequests().antMatchers("/uaa/**")
.permitAll()
.and().csrf().disable() // 禁用csrf
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 无状态
}
}
/**
* Order资源配置
*/
@Configuration
@EnableResourceServer
public class OrderServerConfig extends ResourceServerConfigurerAdapter{
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
resources.resourceId(RESOURCE_ID) //资源ID
.tokenStore(tokenStore) // 本地验证令牌服务
.stateless(true); // 无状态
}
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
// 将uaa全部放行
http.authorizeRequests().antMatchers("/order/**")
// order路径需要ROLE_API 权限
.access("#oauth2.hasAnyScope('ROLE_API')") // 这个scope是授权服务器得授的授权范围
.and().csrf().disable() // 禁用csrf
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 无状态
}
}
}
PS:记得把TokenConfig和WebSecurityConfig拷贝过来,记得将所有路径放行
网关转发token并解析jwt
- 添加网关过滤器
public class AuthFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication instanceof OAuth2Authentication)) {
return null;
}
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
// 获取当前用户身份信息
String principal = oAuth2Authentication.getName();
// 获取当前用户权限信息
List<String> authorities = new ArrayList<>();
oAuth2Authentication.getAuthorities().stream().forEach(v -> authorities.add(v.getAuthority()));
// 把身份和权限信息放在json中,加入http的header中
OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
Map<String,Object> jsonToken = new HashMap<>(requestParameters);
if (oAuth2Authentication != null) {
jsonToken.put("principal",principal);
jsonToken.put("authorities",authorities);
}
currentContext.addZuulRequestHeader("json-token", Base64.encode(JSON.toJSONString(jsonToken)));
// 转发给微服务
return null;
}
}
- 添加zuul配置
@Configuration
public class ZuulConfig {
@Bean
public AuthFilter preFileter() {
return new AuthFilter();
}
@Bean
public FilterRegistrationBean corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setMaxAge(18000L);
source.registerCorsConfiguration("/**", config);
CorsFilter corsFilter = new CorsFilter(source);
FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
- 给资源解析从网关解析出来的token放进去。
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 解析头重的token
String token = httpServletRequest.getHeader("json-token");
if (token != null) {
String tokenJSON = Base64.decodeStr(token);
// 将token转成json对象
JSONObject jsonObject = JSON.parseObject(tokenJSON);
// 用户身份信息
String principal = jsonObject.getString("principal");
UserDTO userDTO = JSON.parseObject(principal, UserDTO.class);
// 用户权限
JSONArray jsonArray = jsonObject.getJSONArray("authorities");
String[] authorities = jsonArray.toArray(new String[jsonArray.size()]);
// 将用户信息和权限填充到用户身份token对象重
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));
// 创建spring security detail
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
// 将authenticationToken填充到安全上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
- 在资源中获取用户信息。
UserDTO userDTO = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
项目地址:https://github.com/yusonghu/spring-security-oauth2-demo