OAuth的思路
OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。“客户端"不能直接登录"服务提供商”,只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料
OAuth 2.0的运行流程如下图,摘自RFC 6749。
文章图片
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
概述 Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实标准。
Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。像所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以轻松扩展以满足自定义需求
特征
- 对身份验证和授权的全面且可扩展的支持
- 防止会话固定、点击劫持、跨站点请求伪造等攻击
- Servlet API 集成
- 与 Spring Web MVC 的可选集成
文章图片
- 客户端发起一个请求,进入 Security 过滤器链。
- 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter;如果不是登出路径则直接进入下一个过滤器。
- 当到 UsernamePasswordAuthenticationFilter的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler登录成功处理器处理,如果不是登录请求则不进入该过滤器。
- 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler鉴权失败处理器处理。
AuthorizationGrantType
1.implicit
2.refresh_token
3.client_credentials
4.password
文章图片
OAuth 定义了四个角色:
- resource owner(资源所有者)
能够授予对受保护资源的访问权限的实体。
当资源所有者是一个人时,它被称为
最终用户。
- resource server(资源服务器)
托管受保护资源的服务器,能够使用访问令牌接受
和响应受保护资源请求。
- client(客户端)
代表
资源所有者并经其授权发出受保护资源请求的应用程序。“客户”一词确实
不暗示任何特定的实现特征(例如,
应用程序是否在服务器、桌面或其他
设备上执行)。
- authorization server(授权服务器)
服务器 在成功验证资源所有者并获得授权
后向客户端颁发访问令牌。
授权服务器和资源服务器之间的交互
超出了本规范的范围。授权服务器
可以是与资源服务器相同的服务器,也可以是单独的实体。
单个授权服务器可以发布多个资源服务器
接受的访问令牌。
文章图片
流程包括以下步骤:
-
(A) 客户端通过将资源所有者的用户代理定向到授权端点来 启动流程。客户端包括 其客户端标识符、请求的范围、本地状态和 一旦授予 (或拒绝)访问权限 ,授权服务器会将用户 代理发送回该URI
-
(B) 授权服务器验证资源所有者(通过用户代理 ) 并确定资源所有者是允许还是拒绝客户端的访问请求。
-
(C) 假设资源所有者授予访问权限,授权服务器使用 之前提供的重定向 URI(在请求中或在 客户端注册期间) 将用户代理重定向回客户端。重定向 URI 包括 授权代码和客户端 之前提供的任何本地状态。
-
(D) 客户端通过包含 在上一步中收到 的授权码,从授权服务器的令牌端点请求访问令牌。发出请求时, 客户端向授权服务器进行身份验证。客户端 包含用于获取授权的重定向URI验证码。
-
(E) 授权服务器对客户端进行身份验证,验证 授权码,并确保接收到的重定向 URI与步骤 (C) 中用于重定向客户端的 URI 匹配。如果有效,授权 服务器 将使用访问令牌和可选的刷新令牌进行响应 。 . 授权请求客户端通过 使用“application/x-www-form-urlencoded”格式 将以下参数添加到授权端点 URI 的查询组件来构造请求 URI , response_type REQUIRED.Value MUST be set to "code".client_id REQUIRED.The client identifier as described in Section 2.2.redirect_uri OPTIONAL.As described in Section 3.1.2.
授权端点 AuthorizationEndpoint
用户授权提交端点 WhitelabelApprovalEndpoint
文章图片
- 用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到, 封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
-
- 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
-
- 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
-
- SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。
- UserDetailsService校验用户信息
- PasswordEncoder会去把数据库查到的密码(一般都是加盐后加密的密码)去进行校验(检查明文密码是否与之前的哈希值匹配)
实现TokenEnhancer接口
package com.macro.mall.auth.component;
import com.macro.mall.auth.domain.SecurityUser;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* JWT内容增强器
* Created by macro on 2020/6/19.
*/
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
/**
* 在创建供客户端使用的新令牌的过程中,提供定制访问令牌的机会(例如,通过其附加信息映射)。
* @param accessToken 当前访问令牌及其过期和刷新令牌
* @param authentication 当前身份验证
* @return 包括客户端和用户详细信息
*/
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
Map info = new HashMap<>();
//把用户ID设置到JWT中
info.put("id", securityUser.getId());
info.put("client_id",securityUser.getClientId());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
SpringSecurity配置
继承WebSecurityConfigurerAdapter启动注解@EnableWebSecurity 打开security安全配置
package com.macro.mall.auth.config;
import org.aspectj.weaver.ast.And;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* SpringSecurity配置
* Created by macro on 2020/6/19.
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Override
protected void configure(HttpSecurity http) throws Exception {
//包含所有执行器端点的匹配器。它还包括链接端点,该端点位于执行器端点的基本路径上
//匹配"/rsa/publicKey"和"/v2/api-docs"规则的放行其他的都需要通过身份验证
http.authorizeRequests()
.requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
.antMatchers("/rsa/publicKey").permitAll()
.antMatchers("/v2/api-docs").permitAll()
.anyRequest().authenticated();
//授权码模式必须配置 http.httpBasic();
http.httpBasic();
}@Bean
@Override
/**
* 身份验证管理器
*/
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}@Bean
/**
密码编码器
*/
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}}
AuthorizationServerTokenServices设置令牌策略
也可以直接在ClientDetailsServiceConfigurer 里面设置
认证服务器配置继承AuthorizationServerConfigurerAdapter 启动注解@EnableAuthorizationServer 打开授权服务器
package com.macro.mall.auth.config;
import com.macro.mall.auth.component.JwtTokenEnhancer;
import com.macro.mall.auth.component.WhitelabelApprovalEndopintHandler;
import com.macro.mall.auth.service.impl.UserServiceImpl;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.*;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
import javax.xml.ws.Service;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.List;
/**
* 认证服务器配置
* Created by macro on 2020/6/19.
*/
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {private final PasswordEncoder passwordEncoder;
private final UserServiceImpl userDetailsService;
private final AuthenticationManager authenticationManager;
private final JwtTokenEnhancer jwtTokenEnhancer;
private final ClientDetailsService clientDetailsService;
@Bean
public AuthorizationCodeServices AuthorizationCodeServices(){
return new InMemoryAuthorizationCodeServices();
}@Bean
public WhitelabelApprovalEndopintHandler whitelabelApprovalEndopintHandler(){
return new WhitelabelApprovalEndopintHandler();
}@Bean
//令牌存储
public TokenStore tokenStore(){
return new InMemoryTokenStore();
}/*@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices tokenServices=new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
//客户端配置策略
tokenServices.setClientDetailsService(clientDetailsService);
//支持令牌的刷新
tokenServices.setSupportRefreshToken(true);
return tokenServices;
}*/
/**
* 客户端详情配置
* 装载Endpoints所有相关的类配置(AuthorizationServer、TokenServices、TokenStore、ClientDetailsService、UserDetailsService)。
* http://localhost:8201/mall-auth/oauth/authorize?response_type=code&client_id=admin-app&redirect_uri=https://www.baidu.com&scope=all
* 授权码模式
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("admin-app")
//加盐
.secret(passwordEncoder.encode("123456"))
//权限
.scopes("all","username")
// 配置 authorization code grant type 配置多个授权模式
.authorizedGrantTypes("password", "refresh_token","authorization_code")
//token有效期
.accessTokenValiditySeconds(3600*24)
//刷新token有效期
.refreshTokenValiditySeconds(3600*24*7)
.autoApprove(false)
.redirectUris("http://localhost:8201/callback")
.and()
.withClient("portal-app")
.secret(passwordEncoder.encode("123456"))// 加盐
.scopes("all","username")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(3600*24)
.refreshTokenValiditySeconds(3600*24*7);
}@Override
//令牌端点服务配置
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//令牌增强器链
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(accessTokenConverter());
enhancerChain.setTokenEnhancers(delegates);
//配置JWT的内容增强器
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService) //配置加载用户信息的服务
.accessTokenConverter(accessTokenConverter())
.authorizationCodeServices(AuthorizationCodeServices())
.tokenStore(tokenStore())
//.tokenServices(tokenServices())
//.pathMapping("/oauth/authorize","/mall-auth/oauth/authorize")
.tokenEnhancer(enhancerChain);
}@Override
//允许客户端进行表单验证 令牌端点安全约束配置
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.
//允许客户端进行表单验证client_id和client_secret做登录认证
allowFormAuthenticationForClients();
}@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(keyPair());
//设置公钥
return jwtAccessTokenConverter;
}@Bean
public KeyPair keyPair() {
//从classpath下的证书中获取秘钥对
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
}}
授权controller,我这里因为是用的微服务架构 getway里面 会默认带一个server_name 会导致授权码模式 不能正常跳转到指定的URI上面
比如请求地址是http://localhost:8201/mall-auth/oauth/authorize?response_type=code&client_id=admin-app&scope=all
授权成功后
会直接跳转到http://localhost:8201:/oauth/authorize
所以我自己定义了一个授权方法 和底层WhitelabelApprovalEndpoint写的差不多 只不过我把下面这段代码注释了
你们如果没有用到getway可以 直接使用默认的不需要重写 /confirm_access 授权请求
文章图片
package com.macro.mall.auth.controller;
import com.macro.mall.auth.component.WhitelabelApprovalEndopintHandler;
import com.macro.mall.auth.domain.Oauth2TokenDto;
import com.macro.mall.common.api.CommonResult;
import com.macro.mall.common.constant.AuthConstant;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint;
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
import org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint;
import org.springframework.stereotype.Controller;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import springfox.documentation.annotations.ApiIgnore;
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
import java.util.Map;
/**
* 自定义Oauth2获取令牌接口
* Created by macro on 2020/7/17.
*/
@Controller
@Api(tags = "AuthController", description = "认证中心登录认证")
@RequestMapping("/oauth")
@SessionAttributes("authorizationRequest")
public class AuthController {@Autowired
//令牌处理器
private TokenEndpoint tokenEndpoint;
@Autowired
//授权处理器
private AuthorizationEndpoint authEndpoint;
@Autowired
WhitelabelApprovalEndopintHandler whitelabelApprovalEndopintHandler;
@ApiOperation("Oauth2获取token")
@ApiImplicitParams({
@ApiImplicitParam(name = "grant_type", value = "https://www.it610.com/article/授权模式", required = true),
@ApiImplicitParam(name = "client_id", value = "https://www.it610.com/article/Oauth2客户端ID", required = true),
@ApiImplicitParam(name = "client_secret", value = "https://www.it610.com/article/Oauth2客户端秘钥", required = true),
@ApiImplicitParam(name = "refresh_token", value = "https://www.it610.com/article/刷新token"),
@ApiImplicitParam(name = "username", value = "https://www.it610.com/article/登录用户名"),
@ApiImplicitParam(name = "password", value = "https://www.it610.com/article/登录密码")
})
@RequestMapping(value = "https://www.it610.com/token", method = RequestMethod.POST)
@ResponseBody
public CommonResult postAccessToken(@ApiIgnore Principal principal, @ApiIgnore @RequestParam Map parameters) throws HttpRequestMethodNotSupportedException {
OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal,parameters).getBody();
Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder()
.token(oAuth2AccessToken.getValue())
.refreshToken(oAuth2AccessToken.getRefreshToken().getValue())
.expiresIn(oAuth2AccessToken.getExpiresIn())
.tokenHead(AuthConstant.JWT_TOKEN_PREFIX).build();
return CommonResult.success(oauth2TokenDto);
}/**
* 授权端点
* @param model
* @param request
* @return
* @throws Exception
*/
@RequestMapping("/confirm_access")
public ModelAndView getAccessConfirmation(Map model, HttpServletRequest request) throws Exception{
return whitelabelApprovalEndopintHandler.getAccessConfirmation(model,request);
}}
重写WhitelabelApprovalEndopint 授权处理器
package com.macro.mall.auth.component;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.util.HtmlUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* @author Xuyijun
* @classname WhitelabelApprovalEndopintHandler.java
* 授权处理器
* @create 2022-02-17, 星期四, 13:38:29
*/public class WhitelabelApprovalEndopintHandler {public ModelAndView getAccessConfirmation(Map model, HttpServletRequest request) throws Exception {
final String approvalContent = createTemplate(model, request);
if (request.getAttribute("_csrf") != null) {
model.put("_csrf", request.getAttribute("_csrf"));
}
View approvalView = new View() {
@Override
public String getContentType() {
return "text/html";
}@Override
public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType(getContentType());
response.getWriter().append(approvalContent);
}
};
return new ModelAndView(approvalView, model);
}protected String createTemplate(Map model, HttpServletRequest request) {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest");
String clientId = authorizationRequest.getClientId();
StringBuilder builder = new StringBuilder();
builder.append("OAuth Approval");
builder.append("Do you authorize \"").append(HtmlUtils.htmlEscape(clientId));
builder.append("\" to access your protected resources?
");
builder.append("";
if (model.containsKey("scopes") || request.getAttribute("scopes") != null) {
builder.append(createScopes(model, request));
builder.append(authorizeInputTemplate);
} else {
builder.append(authorizeInputTemplate);
builder.append("");
}builder.append("");
return builder.toString();
}private CharSequence createScopes(Map model, HttpServletRequest request) {
StringBuilder builder = new StringBuilder("");
@SuppressWarnings("unchecked")
Map scopes = (Map) (model.containsKey("scopes") ?
model.get("scopes") : request.getAttribute("scopes"));
for (String scope : scopes.keySet()) {
String approved = "true".equals(scopes.get(scope)) ? " checked" : "";
String denied = !"true".equals(scopes.get(scope)) ? " checked" : "";
scope = HtmlUtils.htmlEscape(scope);
builder.append("- ");
builder.append(scope).append(": Approve ");
builder.append("Deny
");
}
builder.append("
");
return builder.toString();
}
}
资源服务器我是在getway做的 这里就不写了
http://localhost:8201/mall-auth/oauth/authorize?response_type=code&client_id=admin-app&scope=all
访问
因为我配置了多个 ,选一个 点击授权即可
文章图片
然后可以看到后面code
文章图片
打开postman
http://localhost:8201/mall-auth/oauth/token?grant_type=authorization_code&code=Sq4mTN&redirect_uri=http://localhost:8201/callback&scope=all&client_id=admin-app&client_secret=123456
【java|springcloud-alibaba springSecurity整合oauth2 授权码模式】post请求
文章图片
返回成功
文章图片
参考链接
Oauth2认证流程官方文档https://tools.ietf.org/html/rfc6749#section-1.3
spring security 整合oauth2
oauth自定义登录页面和授权页面
推荐阅读
- flowable工作流技术学习
- JAVA|Spring boot 提示“Whitelabel Error Page”
- Spring|Spring Boot整合dubbo+Nacos实战(二)
- Spring|22-Spring Authorization Server初体验
- 个人领悟|SpringBoot学习笔记
- 个人领悟|在电脑上使用tomcat部署项目时的常见问题
- JavaWeb学习记录——servlet+tomcat+request+respond+JSP实战项目(使用MVC模式开发)
- 编程语言|宁愿“大小周”、每天只写 200 行代码、月薪 8k-17k 人群再涨!揭晓中国开发者真实现状...
- java|7个小技巧,老板再也无法留我加班了...