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 的真正强大之处在于它可以轻松扩展以满足自定义需求
OAuth 2.0授权框架定义了四种标准授权类型:授权码、隐式、资源所有者密码凭据和客户端凭据
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.
令牌端点TokenEndpoint
授权端点 AuthorizationEndpoint
用户授权提交端点 WhitelabelApprovalEndpoint
JWT内容增强器配置
实现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<String, Object> 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<TokenEnhancer> 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 = "授权模式", required = true),
@ApiImplicitParam(name = "client_id", value = "Oauth2客户端ID", required = true),
@ApiImplicitParam(name = "client_secret", value = "Oauth2客户端秘钥", required = true),
@ApiImplicitParam(name = "refresh_token", value = "刷新token"),
@ApiImplicitParam(name = "username", value = "登录用户名"),
@ApiImplicitParam(name = "password", value = "登录密码")
})
@RequestMapping(value = "/token", method = RequestMethod.POST)
@ResponseBody
public CommonResult<Oauth2TokenDto> postAccessToken(@ApiIgnore Principal principal, @ApiIgnore @RequestParam Map<String, String> 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<String, Object> 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<String, Object> 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<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType(getContentType());
response.getWriter().append(approvalContent);
}
};
return new ModelAndView(approvalView, model);
}
protected String createTemplate(Map<String, Object> 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<String, Object> model, HttpServletRequest request) {
StringBuilder builder = new StringBuilder(""
);
@SuppressWarnings("unchecked")
Map<String, String> scopes = (Map<String, String>) (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(": );
builder.append(scope).append("\" value=\"true\"").append(approved).append(">Approve ");
builder.append(").append(scope).append("\" value=\"false\"");
builder.append(denied).append(">Deny ");
}
builder.append("");
return builder.toString();
}
}
资源服务器我是在getway做的 这里就不写了
http://localhost:8201/mall-auth/oauth/authorize?response_type=code&client_id=admin-app&scope=all
访问
因为我配置了多个 ,选一个 点击授权即可
打开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
post请求
返回成功
参考链接
Oauth2认证流程官方文档https://tools.ietf.org/html/rfc6749#section-1.3
spring security 整合oauth2
oauth自定义登录页面和授权页面