前言:
OAuth 2.0 是当前授权领域比较流行的标准,它可以为Web应用程序、桌面应用程序、移动设备等提供简单的客户端开发方式。在SpringCloud中提供了SpringCloud Security组件来构建安全的应用程序。SpringCloud Security主要封装了Spring Security、SpringCloud Security OAuth2 等相关实现,这篇文章我们就来介绍一下如何集成OAuth2.0。
正文:
零. OAuth2.0基础知识:
1. 角色:
- Resource Owner(资源所有者):是能够对受保护的资源授予访问权限的实体,可以是一个用户,这时会称为终端用户。
- Resource Server(资源服务器):持有受保护的资源,允许持有访问令牌的请求访问受保护资源。
- Client(客户端):持有资源所有者的授权,代表资源所有者对受保护资源进行访问。
- Authorization Server(授权服务器):对资源所有者的授权进行认证,成功后向客户端发送访问令牌。
2. 授权流程:
3. 客户端授权类型:
Oauth2.0默认定义了四种授权类型,当然也提供了用于定义额外授权类型的扩展机制。默认的四种授权类型为:
- Authorization Code:授权码类型。
- Implicit :简化类型,也称为隐式类型。
- Resource Owner Password Credentials:密码类型。
- Client Credential:客户端类型。
一、版本信息:
SpringCloud Greenwich版本、Springboot 2.1.4.RELEASE版本、OAuth2.0 2.1.2版本
二、授权服务器配置:
1.引入依赖架包:
<!--oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
2.Springboot启动类上添加@EnableAuthorizationServer注解:
3.配置授权服务器的Oauth2ServiceConfig类:
package com.hanxiaozhang.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
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.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import javax.sql.DataSource;
/**
* 功能描述: <br>
* 〈配置授权服务器〉
*
* @Author:hanxiaozhang
* @Date: 2020/7/27
*/
@Configuration
public class Oauth2ServiceConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
/**
* 配置客户端详情服务
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 客户端id
.withClient("hanxiaozhang")
// 允许请求范围
.scopes("hanxiaozhang")
// 客户端可以使用的授权范围
.authorizedGrantTypes("password", "authorization_code", "refresh_token")
// 客户端安全码
.secret(new BCryptPasswordEncoder().encode("hanxiaozhang"));
}
/**
* 配置Endpoints相关信息:AuthorizationServer、TokenServices、TokenStore、
* ClientDetailsService、UserDetailsService
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.tokenStore(jdbcTokenStore())
.tokenServices(tokenServices());
}
@Primary
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
// access_token:72个小时
tokenServices.setAccessTokenValiditySeconds(60 * 60 * 72);
// refresh_token:72个小时
tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 12);
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(false);
tokenServices.setTokenStore(jdbcTokenStore());
return tokenServices;
}
@Bean
JdbcTokenStore jdbcTokenStore() {
return new JdbcTokenStore(dataSource);
}
/**
* AuthorizationServerSecurityConfigurer继承SecurityConfigurerAdapter,是一个Spring Security安全配置
* 提供给AuthorizationServer去配置AuthorizationServer的端点(/oauth/****)的安全访问规则、过滤器Filter
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
// 获取token的请求不在拦截
.tokenKeyAccess("permitAll()")
// 验证通过返回token信息
.checkTokenAccess("isAuthenticated()")
// 运行客户端使用client_id和client_secret获取token
.allowFormAuthenticationForClients();
}
public static void main(String[] args) {
System.out.println("加密: " + new BCryptPasswordEncoder().encode("hanxiaozhang"));
System.out.println("比对: " + new BCryptPasswordEncoder().matches("123456", "$2a$10$N5WQVfjpdj.v00dgkBW9cu48iKvke8fu1IsuaKRj5rOc/olmrEetW"));
System.out.println("比对: " + new BCryptPasswordEncoder().matches("123456", "$2a$10$Vp1CxPTT/QBmU88jZYzkXOYgIq04Kvfd.o.YYqFn0y6rC5hgO/Yqe"));
}
}
4.配置Spring Security的SecurityConfig类:
package com.hanxiaozhang.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
/**
* 功能描述: <br>
* 〈配置Spring Security〉
*
* @Author:hanxiaozhang
* @Date: 2020/7/27
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetail;
/**
* 配置认证管理器
*
* @return
* @throws Exception
*/
@Override
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭跨域保护
.csrf().disable()
// 配置HttpSecurity接收的请求
.requestMatchers()
.antMatchers("/login","/oauth/**")
.and()
// 配置URL的权限
.authorizeRequests()
// 访问/login,/oauth/**不要权限验证,这个配置没有卵用吧,应该配置资源服务的
.antMatchers("/login","/oauth/**").permitAll()
// 其他所有路径都需要权限校验
.anyRequest().authenticated()
.and()
// [解决访问/oauth/authorize/**返回403的问题]
.httpBasic();
}
/**
* 配置用户
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetail)
.passwordEncoder(passwordEncoder());
}
/**
* 配置密码解密器
*
* @return
*/
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
5.重写实现UserDetailsService类:
package com.hanxiaozhang.system.service.impl;
import com.hanxiaozhang.exception.UserNotLoginException;
import com.hanxiaozhang.security.CurrentUser;
import com.hanxiaozhang.system.dao.MenuDao;
import com.hanxiaozhang.system.dao.UserDao;
import com.hanxiaozhang.system.entity.UserEntity;
import com.hanxiaozhang.util.StringUtil;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 功能描述: <br>
* 〈〉
*
* @Author:hanxiaozhang
* @Date: 2020/7/27
*/
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserDao userDao;
@Resource
private MenuDao menuDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = userDao.getByUsername(username);
if (user == null) {
throw new UserNotLoginException("用户名不存在!");
}
List<String> permission = menuDao.listPermsByUserId(user.getUserId());
// 权限
Set<GrantedAuthority> authorities = permission
.stream().filter(StringUtil::isNotBlank).map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
return new CurrentUser(username, user.getPassword(), user.getUserId(), user.getName(),authorities);
}
}
6.获取当前用户工具类CurrentUserUtil:
package com.hanxiaozhang.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import java.util.LinkedHashMap;
/**
* 功能描述: <br>
* 〈权限工具类〉
*
* @Author:hanxiaozhang
* @Date: 2021/3/24
*/
public class CurrentUserUtil {
/**
* 获取Principal
*
* @return
*/
private static Object getPrincipal() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof OAuth2Authentication) {
Object principal = ((OAuth2Authentication) authentication).getUserAuthentication().getPrincipal();
if (principal instanceof CurrentUser) {
return principal;
}
Object details = ((OAuth2Authentication) authentication).getUserAuthentication().getDetails();
if (details instanceof LinkedHashMap && details != null) {
principal = ((LinkedHashMap) details).get("principal");
if (principal instanceof LinkedHashMap && principal != null) {
return principal;
}
}
}
return null;
}
/**
* 获取当前用户id
*
* @return
*/
public static Long getUserId() {
if (getPrincipal() instanceof CurrentUser) {
return ((CurrentUser) getPrincipal()).getUserId();
}
return getPrincipal() == null ? null : ((LinkedHashMap) getPrincipal()).get("userId") == null ? null : ((Integer) ((LinkedHashMap) getPrincipal()).get("userId")).longValue();
}
/**
* 获取当前用户名
*
* @return
*/
public static String getName() {
if (getPrincipal() instanceof CurrentUser) {
return ((CurrentUser) getPrincipal()).getName();
}
return getPrincipal() == null ? null : ((LinkedHashMap) getPrincipal()).get("name") == null ? null : (String) ((LinkedHashMap) getPrincipal()).get("name");
}
/**
* 获取当前用户名称
*
* @return
*/
public static String getUserName() {
if (getPrincipal() instanceof CurrentUser) {
return ((CurrentUser) getPrincipal()).getUsername();
}
return getPrincipal() == null ? null : ((LinkedHashMap) getPrincipal()).get("username") == null ? null : (String) ((LinkedHashMap) getPrincipal()).get("username");
}
public static void main(String[] args) {
Object object = null;
String str = null;
System.out.printf((String) str);
System.out.printf((String) object);
System.out.printf(String.valueOf(object));
}
}
三、资源服务器配置:
1.引入依赖架包:
<!--oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
2.资源服务器配置类ResourceServerConfig:
package com.hanxiaozhang.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.web.context.request.RequestContextListener;
import javax.servlet.http.HttpServletResponse;
/**
* 功能描述: <br>
* 〈资源服务器配置类〉
*
* @Author:hanxiaozhang
* @Date: 2020/7/31
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
// 配置异常处理器
.antMatcher("/**")
.exceptionHandling()
.authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
// 所有请求都需要授权,除去/actuator/**
.authorizeRequests()
.antMatchers("/actuator/**").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();
}
/**
* 解决如下问题:
* Could not fetch user details: class org.springframework.beans.factory.BeanCreationException,
* Error creating bean with name 'scopedTarget.oauth2ClientContext':
* Scope 'request' is not active for the current thread;
*
*
* @return
*/
@Bean
@Order(0)
public RequestContextListener requestContextListener() {
return new RequestContextListener();
}
}
3.如果资源服务器与授权服务器不在一个服务,需要配置YAML文件:
security:
oauth2:
client:
access-token-uri: http://localhost:9091/oauth/token
user-authorization-uri: http://localhost:9091/oauth/authorize
client-id: hanxiaozhang
client-secret: $2a$10$pjIVEPMujuzQ1m1CWKkZneHhvQUAW1Y/DooXkbl6gApGakwQSy6..
clientAuthenticationScheme: form
resource:
user-info-uri: http://localhost:9091/token/user # 指定user info的URI
prefer-token-info: true # 是否使用token info,默认为true
authorization:
check-token-access: http://localhost:9091/oauth/check_token
四、使用:
1.模拟登陆,获取access_token:
http://localhost:9091/oauth/token?username=admin&password=123456&grant_type=password&scope=hanxiaozhang&client_id=hanxiaozhang&client_secret=hanxiaozhang
2. 使用access_token获取资源,把access_token放到header中:
http://localhost:9091/currentUser
五、遇到问题:
1.springboot 2.x版本自己集成了spring security,但是在部分微服务中需要去除它,解决方法:
在@SpringBootApplication注解中移除SecurityAutoConfiguration.class和ManagementWebSecurityAutoConfiguration.class