尼恩说在前面
说说:OAuth2.0 的四种授权方式,说说是哪四种? 说说:Spring Security的原理? 说说:Spring Security是如何实现 OAuth2.0 ?
SpringSecurity& Auth2.0 学习圣经: 从入门到精通 SpringSecurity& Auth2.0 Sa-Token学习圣经: 从入门到精通Sa-Token
本文目录
- 尼恩说在前面
- 1 安全认证的基本概念
- 1.1 认证
- 1.2 授权(鉴权)
- 2 Spring Security 核心组件
- 2.1 凭证 Authentication
- 2.2 认证提供者 AuthenticationProvider
- 2.3 认证管理者 AuthenticationManager
- 3 Spring Security入门案例
- 4 Spring Security进阶
- 4.1 简单认证流程实战
- 4.2 基于数据源认证流程实战
-4.2.1 常见内置类
-4.2.2 基于数据源认证实战
- 5 授权(鉴权)控制
- 6 Spring Security过滤器
- 面试题:说说Spring Security的原理?
- 7 Spring Security Oauth2
- 7.1 什么是单点登录?解决什么问题?
- 7.2 OAuth2是什么?
- 7.3 OAuth2核心概念
-7.3.1 OAuth2角色
-7.3.2 OAuth2授权模式
- 7.4. Spring Security Oauth2.0实战
-7.4.1 授权服务器(SSO-Server)
-7.4.2 资源服务器 (SSO-Client)
-7.4.3 授权模式实战
-7.4.3.1 授权码模式
-QQ登录的授权码模式(Authorization Code Grant)登录流程
-Spring Security Oauth2.0 授权码模式(Authorization Code Grant)登录流程
-Spring Security Oauth2.0 角色设置
-授权码模式的第一步:申请授权码
-授权码模式的第2步:申请令牌
-授权码模式的第3步:令牌校验
-授权码模式的第4步:使用令牌
-如何 通过数据库来管理 OAuth2客户端?
-1. 创建客户端详细信息的数据库表
-2. 配置 `ClientDetailsServiceConfigurer` 使用数据库
-3. 配置数据源
-7.4.3.2 密码模式
-密码模式的核心要点
-密码模式的第一步:申请令牌
-密码模式的第2步:令牌校验
-密码模式的第3步:使用令牌
-7.4.3.3 简化模式
-简化模式的要点:
-简化模式第一步:申请令牌
-简化模式第2步:使用令牌
-7.4.3.4 客户端模式
-客户端模式的要点:
- 说在最后:有问题找老架构取经
1 安全认证的基本概念
认证(Authentication) 授权(Authorization)
1.1 认证
如果校验通过,则:正常返回数据。 如果校验未通过,则:抛出异常,告知其需要先进行登录。
用户提交 name
+password
参数,调用登录接口。登录成功,返回这个用户的 Token 会话凭证。 用户后续的每次请求,都携带上这个 Token。 服务器根据 Token 判断此会话是否登录成功。
1.2 授权(鉴权)
有,就让你通过。 没有?那么禁止访问!
["user-add", "user-delete", "user-get"]
,这时候我来校验权限 "user-update"
,则其结果就是:验证失败,禁止访问。2 Spring Security 核心组件
Authentication:认证,其实就是身份的识别,也就是登录 Authorization:授权(鉴权),根据角色进行权限控制
2.1 凭证 Authentication
public interface Authentication extends Principal, Serializable {
//权限集合
//可使用 AuthorityUtils.commaSeparatedStringToAuthorityList("admin, ROLE_ADMIN")进行初始化
Collection<? extends GrantedAuthority> getAuthorities();
//用户名和密码认证时,可以理解为密码
Object getCredentials();
//认证时包含的一些详细信息,可以是一个包含用户信息的 POJO 实例
Object getDetails();
//用户名和密码认证时,可以理解为用户名
Object getPrincipal();
//是否认证通过,通过为 true
boolean isAuthenticated();
//设置是否认证通过
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
UsernamePasswordAuthenticationToken: 用户名、密码认证的场景中作为验证的凭证 RememberMeAuthenticationToken: “记住我”的身份认证场景 AnonymousAuthenticationToken: 匿名访问的用户
2.2 认证提供者 AuthenticationProvider
public interface AuthenticationProvider {
//对实参 authentication 进行身份认证操作
Authentication authenticate(Authentication authentication) throws AuthenticationException;
//判断是否支持该 authentication
boolean supports(Class<?> authentication);
}
AbstractUserDetailsAuthenticationProvider: 对 UsernamePasswordAuthenticationToken 类型的凭证/令牌进行验证的认证提供者类,用于“用户名+密码”验证的场景。 RememberMeAuthenticationProvider: 对 RememberMeAuthenticationToken 类型的凭证/令牌进行验证的认证提供者类,用于“记住我”的身份认证场景。 AnonymousAuthenticationProvider: 这是一个对 AnonymousAuthenticationToken 类型的凭证/令牌进行验证的认证提供者类,用于匿名身份认证场景。
2.3 认证管理者 AuthenticationManager
public interface AuthenticationManager {
//认证流程的入口
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
//提供者清单
private List<AuthenticationProvider> providers = Collections.emptyList();
//迭代提供者清单,找出支持令牌的提供者,交给提供者去执行令牌验证
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
}
}
3 Spring Security入门案例
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
HelloController
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "欢迎访问 hangge.com";
}
}
启动
配置用户名和密码
spring.security.user.name=lxs
spring.security.user.password=1
spring.security.user.roles=admin
4 Spring Security进阶
4.1 简单认证流程实战
定制一个凭证/令牌类。 定制一个认证提供者类和凭证/令牌类进行配套,并完成对自制凭证/令牌实例的验证。 定制一个过滤器类,从请求中获取用户信息组装成定制凭证/令牌,交给认证管理者。 定制一个 HTTP 的安全认证配置类(AbstractHttpConfigurer 子类),将上一步定制的过滤器加入请求的过滤链。 定义一个 Spring Security 安全配置类(WebSecurityConfigurerAdapter 子类), 对 Web容器的 HTTP 安全认证机制进行配置。
DemoToken
public class DemoToken extends AbstractAuthenticationToken
{
//用户名称
private String userName;
//密码
private String password;
...
}
DemoAuthProvider
public class DemoAuthProvider implements AuthenticationProvider {
public DemoAuthProvider(){
}
//模拟的数据源,实际场景从 DB 中获取
private Map<String, String> map = new LinkedHashMap<>();
//初始化模拟的数据源,放入两个用户
{
map.put("zhangsan", "123456" );
map.put("lisi", "123456" );
}
//具体的验证令牌方法
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
DemoToken token = (DemoToken) authentication;
//从数据源 map 中获取用户密码
String rawPass = map.get(token.getUserName());
//验证密码,如果不相等,就抛出异常
if (!token.getPassword().equals(rawPass))
{
token.setAuthenticated(false);
throw new BadCredentialsException("认证有误:令牌校验失败" );
}
//验证成功
token.setAuthenticated(true);
return token;
}
/**
*判断令牌是否被支持
*@param authentication 这里仅仅 DemoToken 令牌被支持
*@return
*/
@Override
public boolean supports(Class<?> authentication)
{
return authentication.isAssignableFrom(DemoToken.class);
}
}
DemoAuthFilter
public class DemoAuthFilter extends OncePerRequestFilter
{
//认证失败的处理器
private AuthenticationFailureHandler failureHandler = new AuthFailureHandler();
...
//authenticationManager 是认证流程的入口,接收一个 Authentication 令牌对象作为参数
private AuthenticationManager authenticationManager;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException
{
...
AuthenticationException failed = null;
try
{
Authentication returnToken=null;
boolean succeed=false;
//从请求头中获取认证信息
String token = request.getHeader(SessionConstants.AUTHORIZATION_HEAD);
String[] parts = token.split(",");
//组装令牌
DemoToken demoToken = new DemoToken(parts[0],parts[1]);
//提交给 AuthenticationManager 进行令牌验证
returnToken = (DemoToken) this.getAuthenticationManager().authenticate(demoToken);
//获取认证成功标志
succeed=demoToken.isAuthenticated();
if (succeed)
{
//认证成功,设置上下文令牌
SecurityContextHolder.getContext().setAuthentication(returnToken);
//执行后续的操作
filterChain.doFilter(request, response);
return;
}
} catch (Exception e)
{
logger.error("认证有误", e);
failed = new AuthenticationServiceException("请求头认证消息格式错误",e );
}
if(failed == null)
{
failed = new AuthenticationServiceException("认证失败");
}
//认证失败了
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, failed);
}
...
}
AbstractHttpConfigurer
public class DemoAuthConfigurer<T extends DemoAuthConfigurer<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T,B>
{
//创建认证过滤器
private DemoAuthFilter authFilter = new DemoAuthFilter();
//将过滤器加入 http 过滤处理责任链
@Override
public void configure(B http) throws Exception
{
//获取 Spring Security 共享的 AuthenticationManager 认证管理者实例
//将其设置到认证过滤器
authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
DemoAuthFilter filter = postProcess(authFilter);
//将过滤器加入 http 过滤处理责任链
http.addFilterBefore(filter, LogoutFilter.class);
}
}
DemoWebSecurityConfig
应用DemoAuthConfigurer 配置类; 构造 AuthenticationManagerBuilder 认证管理者实例。
@EnableWebSecurity
public class DemoWebSecurityConfig extends WebSecurityConfigurerAdapter
{
//配置 HTTP 请求的安全策略,应用 DemoAuthConfigurer 配置类实例
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().disable()
...
.and()
//应用 DemoAuthConfigurer 配置类
.apply(new DemoAuthConfigurer<>())
.and()
.sessionManagement().disable();
}
//配置认证 Builder,由其负责构造 AuthenticationManager 认证管理者实例
//Builder 将构造 AuthenticationManager 实例,并且作为 HTTP 请求的共享对象存储
//在代码中可以通过 http.getSharedObject(AuthenticationManager.class) 来获取管理者实例
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
//加入自定义的 Provider 认证提供者实例
auth.authenticationProvider(demoAuthProvider());
}
//自定义的认证提供者实例
@Bean("demoAuthProvider" )
protected DemoAuthProvider demoAuthProvider()
{
return new DemoAuthProvider();
}
}
执行测试
4.2 基于数据源认证流程实战
4.2.1 常见内置类
UsernamePasswordAuthenticationToken
AbstractUserDetailsAuthenticationProvider
UserDetailsService
public interface UserDetailsService {
//通过用户名从数据源加载用户信息
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetails
public interface UserDetails extends Serializable {
//权限集合
Collection<? extends GrantedAuthority> getAuthorities();
//密码,一般为密文
String getPassword();
//用户名
String getUsername();
//用户名是否未过期
boolean isAccountNonExpired();
//用户名是否未锁定
boolean isAccountNonLocked();
//用户密码是否未过期
boolean isCredentialsNonExpired();
//账号是否可用(可理解为是否删除)
boolean isEnabled();
}
PasswordEncoder
public interface PasswordEncoder {
//对明文 rawPassword 加密
String encode(CharSequence rawPassword);
//判断 rawPassword 与 encodedPassword 是否匹配
boolean matches(CharSequence rawPassword, String encodedPassword);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
4.2.2 基于数据源认证实战
定制一个凭证/令牌类
//方式二:数据库 认证演示
UserDetails userDetails = User.builder()
.username(parts[0])
.password(parts[1])
.authorities(USER_INFO)
.build();
//创建一个用户名+密码的凭证,一般情况下,这里的密码需要明文
Authentication userPassToken = new UsernamePasswordAuthenticationToken(userDetails,
userDetails.getPassword(),
userDetails.getAuthorities());
//进入认证流程
returnToken = this.getAuthenticationManager().authenticate(userPassToken);
succeed = userPassToken.isAuthenticated();
//方式二end
认证提供者
@EnableWebSecurity
public class DemoWebSecurityConfig extends WebSecurityConfigurerAdapter
{
...
//注入全局 BCryptPasswordEncoder 加密器容器实例
@Resource
private PasswordEncoder passwordEncoder;
//注入数据源服务容器实例
@Resource
private DemoAuthUserService demoUserAuthService;
@Bean("daoAuthenticationProvider")
protected AuthenticationProvider daoAuthenticationProvider() throws Exception
{
//创建一个数据源提供者
DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
//设置加密器
daoProvider.setPasswordEncoder(passwordEncoder);
//设置用户数据源服务
daoProvider.setUserDetailsService(demoUserAuthService);
return daoProvider;
}
}
DemoAuthUserService
@Slf4j
@Service
public class DemoAuthUserService implements UserDetailsService
{
//模拟的数据源,实际从 DB 中获取
private Map<String, String> map = new LinkedHashMap<>();
//初始化模拟的数据源,放入两个用户
{
map.put("zhangsan", "123456");
map.put("lisi", "123456");
}
/**
*装载系统配置的加密器
*/
@Resource
private PasswordEncoder passwordEncoder;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
//实际场景中需要从数据库加载用户
//这里出于演示的目的, 用 map 模拟真实的数据源
String password = map.get(username);
if (password == null)
{
return null;
}
if (null == passwordEncoder)
{
passwordEncoder = new BCryptPasswordEncoder();
}
/**
*返回一个用户详细实例,包含用户名、加密后的密码、用户权限清单、用户角色
*/
UserDetails userDetails = User.builder()
.username(username)
.password(passwordEncoder.encode(password))
.authorities(SessionConstants.USER_INFO)
.roles("USER")
.build();
return userDetails;
}
}
5 授权(鉴权)控制
@PreAuthorize @PostAuthorize @PreFilter和 @PostFilter。
EnableGlobalMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
TestController
@RestController
public class TestController {
Logger logger = LoggerFactory.getLogger(TestController.class);
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
return "product id : " + id;
}
@GetMapping("/order/{id}")
public String getOrder(@PathVariable String id) {
return "order id : " + id;
}
@GetMapping("/book/{id}")
public String getBook(@PathVariable String id) {
return "book id : " + id;
}
@GetMapping("/anno/{id}")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String getAnno(@PathVariable String id) {
return "admin id :" + id;
}
@RequestMapping("/hello")
@PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
public String hello() {
return "hello you ...";
}
@GetMapping("/getPrinciple")
public OAuth2Authentication getPrinciple(OAuth2Authentication oAuth2Authentication, Principal principal, Authentication authentication) {
logger.info(oAuth2Authentication.getUserAuthentication().getAuthorities().toString());
logger.info(oAuth2Authentication.toString());
logger.info("principal.toString() " + principal.toString());
logger.info("principal.getName() " + principal.getName());
logger.info("authentication: " + authentication.getAuthorities().toString());
return oAuth2Authentication;
}
}
控制权限方式
SecurityContextHolder.getContext().getAuthentication() //使用工具方法
@Resource
Authentication authentication //注入的方法
6 Spring Security过滤器
Configurer | Filter | 功能说明 |
EnableWebSecurity
WebSecurityConfiguration
创建springSecurityFilterChain
WebSecurity
HttpSecurity
protected void configure(HttpSecurity http) throws Exception {
this.logger.debug("Using default configure(HttpSecurity). "
+ "If subclassed this will potentially override subclass configure(HttpSecurity).");
http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin();
http.httpBasic();
}
@Override
protected DefaultSecurityFilterChain performBuild() {
this.filters.sort(OrderComparator.INSTANCE);
List<Filter> sortedFilters = new ArrayList<>(this.filters.size());
for (Filter filter : this.filters) {
sortedFilters.add(((OrderedFilter) filter).filter);
}
return new DefaultSecurityFilterChain(this.requestMatcher, sortedFilters);
}
面试题:说说Spring Security的原理?
7 Spring Security Oauth2
7.1 什么是单点登录?解决什么问题?
7.2 OAuth2是什么?
在京东官网点击qq登录
跳转到qq登录页面输入用户名密码,然后点授权并登录
调回到京东页面,成功登录
7.3 OAuth2核心概念
7.3.1 OAuth2角色
客户端 Client:第三方应用,比如上面的 京东 资源所有者 Resource Owner:资源所有者,即用户 授权服务器 Authorization Server:授权服务器,即提供第三方登录服务的服务器,如QQ 资源服务器 Resource Server:拥有资源信息的服务器,通常和授权服务器属于同一应用,如QQ
7.3.2 OAuth2授权模式
授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。 它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本 都是使用这种模式。 简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器中请令牌,一般若网站是纯静态页面,则可以采用这种方式。 密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器中请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。
7.4. Spring Security Oauth2.0实战
7.4.1 授权服务器(SSO-Server)
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
oauth2配置类
@Configuration
//开启授权服务
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
private static final String CLIENT_ID = "cms";
private static final String SECRET_CHAR_SEQUENCE = "{noop}secret";
private static final String SCOPE_READ = "read";
private static final String SCOPE_WRITE = "write";
private static final String TRUST = "trust";
private static final String USER ="user";
private static final String ALL = "all";
private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 30*60;
private static final int FREFRESH_TOKEN_VALIDITY_SECONDS = 30*60;
// 密码模式授权模式
private static final String GRANT_TYPE_PASSWORD = "password";
//授权码模式
private static final String AUTHORIZATION_CODE = "authorization_code";
//refresh token模式
private static final String REFRESH_TOKEN = "refresh_token";
//简化授权模式
private static final String IMPLICIT = "implicit";
//指定哪些资源是需要授权验证的
private static final String RESOURCE_ID = "resource_id";
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
// 使用内存存储
.inMemory()
//标记客户端id
.withClient(CLIENT_ID)
//客户端安全码
.secret(SECRET_CHAR_SEQUENCE)
//为true 直接自动授权成功返回code
.autoApprove(true)
.redirectUris("http://127.0.0.1:8084/cms/login") //重定向uri
//允许授权范围
.scopes(ALL)
//token 时间秒
.accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
//刷新token 时间 秒
.refreshTokenValiditySeconds(FREFRESH_TOKEN_VALIDITY_SECONDS)
//允许授权类型
.authorizedGrantTypes(GRANT_TYPE_PASSWORD,AUTHORIZATION_CODE);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 使用内存保存生成的token
endpoints.authenticationManager(authenticationManager).tokenStore(memoryTokenStore());
}
/**
* 认证服务器的安全配置
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
//.realm(RESOURCE_ID)
// 开启/oauth/token_key验证端口认证权限访问
.tokenKeyAccess("isAuthenticated()")
// 开启/oauth/check_token验证端口认证权限访问
// .checkTokenAccess("isAuthenticated()")
.checkTokenAccess("permitAll()")
//允许表单认证
.allowFormAuthenticationForClients();
}
@Bean
public TokenStore memoryTokenStore() {
// 最基本的InMemoryTokenStore生成token
return new InMemoryTokenStore();
}
}
Spring Security配置类
@Configuration
@EnableWebSecurity
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception { //auth.inMemoryAuthentication()
auth.inMemoryAuthentication()
.withUser("lxs")
.password("{noop}123")
.roles("admin");
}
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/asserts/**");
web.ignoring().antMatchers("/favicon.ico");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http // 配置登录页并允许访问
.formLogin().permitAll()
// 配置Basic登录
//.and().httpBasic()
// 配置登出页面
.and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
// 配置允许访问的链接
.and().authorizeRequests().antMatchers("/oauth/**", "/login/**", "/logout/**", "/api/**").permitAll()
// 其余所有请求全部需要鉴权认证
.anyRequest().authenticated()
// 关闭跨域保护;
.and().csrf().disable();
}
}
测试
7.4.2 资源服务器 (SSO-Client)
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
启动器配置
package com.lxs.oauth2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
@SpringBootApplication
@EnableResourceServer
public class CmsApplication {
public static void main(String[] args) {
SpringApplication.run(CmsApplication.class, args);
}
}
oauth2配置类
@Configuration
public class Oauth2ResourceServerConfiguration extends
ResourceServerConfigurerAdapter {
private static final String CHECK_TOKEN_URL = "http://localhost:8888/oauth/check_token";
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl(CHECK_TOKEN_URL);
tokenService.setClientId("cms");
tokenService.setClientSecret("secret");
resources.tokenServices(tokenService);
}
}
spring security配置类
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").authenticated();
// 禁用CSRF
http.csrf().disable();
}
}
Controller
@RestController
public class HelloController {
@GetMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication) {
return authentication;
}
@GetMapping("/index")
public String index() {
return "index";
}
}
测试
7.4.3 授权模式实战
授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本 都是使用这种模式。 简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器中请令牌,一般若网站是纯静态页面,则可以采用这种方式。 密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器中请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。
7.4.3.1 授权码模式
QQ登录的授权码模式(Authorization Code Grant)登录流程
Spring Security Oauth2.0 授权码模式(Authorization Code Grant)登录流程
(A)客户端携带client_id、redirect_uri,中间通过代理者user-Agent (如浏览器 )访问授权服务器,如果已经登录过会直接返回redirect_uri,没有登录过,就重定向 跳转到登录页面 (B)授权服务器对客户端进行身份验证(通过用户代理,让用户输入用户名和密码) (C)授权通过,会重定向到redirect_uri, 并携带授权码code作为uri参数 (D)客户端携带授权码访问授权服务器 (E)验证授权码通过,返回acceptToken
第一步:从授权服务器,获取授权码 code: eg:oauthServer+"/oauth/authorize?client_id="+clientId+"&response_type=code&redirect_uri="+redirectUrl+"&scope=all" 如果没有登录,则会跳转到统一身份认证登录页面。 如果用户登录了,调用接口后,会拿到授权码code,然后 重定向到redirect_uri,授权码会作为redirect_uri的参数 第二步:获取access_token eg:oauthServer+"/oauth/token?code="+code+"&grant_type=authorization_code&client_secret="+clientSecret+"&redirect_uri="+redirectUri+"&client_id="+clientId 大致的响应信息
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1MzQ5NzMsInVzZXJfbmFtZSI6Im5pY2t5IiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiJmMjM0M2Q0NC1hODViLTQyOGYtOWE1ZS1iNTE4NTAwNTM5ODgiLCJjbGllbnRfaWQiOiJvYSIsInNjb3BlIjpbImFsbCJdfQ.LWkN2gC2dBrGTn5uSPzfdW6yRj7jhlX87EE8scY02hI",
"token_type": "bearer",
"expires_in": 59,
"scope": "all",
"user_name": "nicky",
"jti": "f2343d44-a85b-428f-9a5e-b51850053988"
}
第三步:访问系统资源,此时统一认证服务会根据该认证客户端权限信息判断,决定是否返回信息。
访问:
http://localhost:8084/api/userinfo?access_token=${accept_token}
Spring Security Oauth2.0 角色设置
资源所有者(Resource Owner) 用户代理(User Agent) 客户端(Client) 授权服务器(Authorization Server) 资源服务器(Resource Server)
定义资源服务器,用注解 @EnableResourceServer; 定义授权服务器,用注解 @EnableAuthorizationServer;
授权码模式的第一步:申请授权码
Get请求:
http://localhost:8888/oauth/authorize?client_id=cms&client_secret=secret&response_type=code
client_id:客户端id,和授权配置类中设置的客户端id一致。
response_type:授权码模式固定为code
scop:客户端范围,和授权配置类中设置的scop一致。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)
http://localhost:8888/oauth/authorize?client_id=cms&client_secret=secret&response_type=code
http://localhost:8888/login
http .formLogin().permitAll();
http.httpBasic();
当然,弹窗登录 配置是没有登录页面的,http.formLogin().loginPage("/login").permitAll()
,授权码模式的第2步:申请令牌
Post请求
http://localhost:8888/oauth/token?code=H45yPy&grant_type=authorization_code&redirect_uri=http://127.0.0.1:8084/cms/login&scope=all
http://127.0.0.1:8084/cms/login?code=H45yPy
grant_type:授权类型,填写authorization_code,表示授权码模式
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
Base64.getEncoder.encodeToString("客户端ID:客户端密码"),
Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA=
access_token:访问令牌,携带此令牌访问资源
token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer Token(http://www.rfcreader.com/#rfc6750)。
refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
expires_in:过期时间,单位为秒。
scope:范围,与定义的客户端范围一致。
jti:当前token的唯一标识
授权码模式的第3步:令牌校验
Get:
http://localhost:8888/oauth/check_token?token=171ce96e-7492-4a27-becd-8ccbdc69666b
授权码模式的第4步:使用令牌
使用正确令牌访问/index服务
不用令牌访问/index服务
使用错误令牌访问
如何 通过数据库来管理 OAuth2客户端?
ClientDetailsServiceConfigurer
是用来配置OAuth2客户端详情的,可以通过数据库来管理这些客户端的详细信息。ClientDetailsServiceConfigurer
呢?1. 创建客户端详细信息的数据库表
oauth_client_details
。CREATE TABLE oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
2. 配置 ClientDetailsServiceConfigurer 使用数据库
AuthorizationServerConfigurerAdapter
实现类中,通过 ClientDetailsServiceConfigurer
来配置客户端详细信息服务。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.provider.client.JdbcClientDetailsService;
import javax.sql.DataSource;
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 使用JDBC方式从数据库加载客户端信息
clients.withClientDetails(clientDetailsService());
}
public JdbcClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
}
3. 配置数据源
application.properties
或 application.yml
中正确配置了数据库连接:spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
7.4.3.2 密码模式
(A)用户访问客户端,提供URI连接并包含授权服务器的 用户名和密码信息 给 业务服务,业务服务会带着这些信息去访问 授权服务器 (B)授权服务器对客户端进行身份验证 (C)授权通过,返回acceptToken给客户端
密码模式的核心要点
密码模式的第一步:申请令牌
grant_type:授权类型,填写password,表示密码模式
username:用户名
password:密码
access_token:访问令牌,携带此令牌访问资源
token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer Token(http://www.rfcreader.com/#rfc6750)。
refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
expires_in:过期时间,单位为秒。
scope:范围,与定义的客户端范围一致。
jti:当前token的唯一标识
密码模式的第2步:令牌校验
Get:
http://localhost:8888/oauth/check_token?token=1e628350-5711-4983-9b10-da7a7e8b9558
密码模式的第3步:使用令牌
使用正确令牌访问/index服务
不用令牌访问/index服务
使用错误令牌访问
7.4.3.3 简化模式
(A):客户端携带client_id、redirect_uri,中间通过代理者访问授权服务器,如果已经登录过会直接返回redirect_uri,没有登录过就跳转到登录页面 (B)授权服务器对客户端进行身份验证(通过用户代理如浏览器,让用户输入用户名和密码) (C)授权通过,会重定向到redirect_uri并携带授权码token作为uri参数 (D)客户端携带授权码访问资源服务器 (E)验证token通过,返回资源
简化模式的要点:
简化模式第一步:申请令牌
Get请求:
http://localhost:8888/oauth/authorize?client_id=cms&redirect_uri=http://127.0.0.1:8084/cms/login&response_type=token&scope=all
client_id:客户端id,和授权配置类中设置的客户端id一致。
response_type:简化模式固定为token
scop:客户端范围,和授权配置类中设置的scop一致。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址
http .formLogin().permitAll();
,如果要弹窗登录的,可以配置http.httpBasic();
,这种配置是没有登录页面的,自定义登录页面可以这样配置http.formLogin().loginPage("/login").permitAll()
,参考OAuth2Config代码简化模式第2步:使用令牌
使用正确令牌访问/index服务
不用令牌访问/index服务
使用错误令牌访问
简化模式和授权码模式的区别
7.4.3.4 客户端模式
客户端模式的要点:
第一步: 获取token http://localhost:8888/oauth/token?client_id=cms&client_secret=secret&grant_type=client_credentials&scope=all
第二步:拿到acceptToken之后,就可以直接访问资源
申请令牌
post请求:
http://localhost:8888/oauth/token?client_id=cms&client_secret=secret&grant_type=client_credentials&scope=all
client_id:客户端id,和授权配置类中设置的客户端id一致。
client_secret:客户端秘钥,和授权配置类中设置的客户端secret一致。
response_type:密码模式固定为client_credentials
scop:客户端范围,和授权配置类中设置的scop一致。
使用令牌 使用正确令牌访问/index服务
不用令牌访问/index服务
使用错误令牌访问
说在最后:有问题找老架构取经
被裁之后, 空窗1年/空窗2年, 如何 起死回生 ?
案例1:42岁被裁2年,天快塌了,急救1个月,拿到开发经理offer,起死回生
案例2:35岁被裁6个月, 职业绝望,转架构急救上岸,DDD和3高项目太重要了
案例3:失业15个月,学习40天拿offer, 绝境翻盘,如何实现?
被裁之后,100W 年薪 到手, 如何 人生逆袭?
100W案例,100W年薪的底层逻辑是什么? 如何实现年薪百万? 如何远离 中年危机?
如何 逆天改命,包含AI、大数据、golang、Java 等
实现职业转型,极速上岸
关注职业救助站公众号,获取每天职业干货
助您实现职业转型、职业升级、极速上岸
---------------------------------
实现架构转型,再无中年危机
关注技术自由圈公众号,获取每天技术千货
一起成为牛逼的未来超级架构师
几十篇架构笔记、5000页面试宝典、20个技术圣经
请加尼恩个人微信 免费拿走
暗号,请在 公众号后台 发送消息:领电子书
如有收获,请点击底部的"在看"和"赞",谢谢