备:附件中OAuth2 授权服务器实现源码及PPT
一、Authorization code grant
The flow illustrated in Figure 1 includes the following steps:
- (A) The client (typically, a web application) initiates the flow by directing the
- resource owner's user agent (typically, a web browser) to the authorization
- endpoint. The client's request includes the client identifier, requested scope,
- local state, and a redirection URI. The authorization server directs the user
- agent (typically, a web browser) back to the redirect URI after the access is
- granted (or denied).
- (B) The resource owner authenticates with the authorization server through
- the user agent and grants or denies the client's access request.
- (C) If the resource owner grants access, the authorization server redirects the
- user agent (typically, a web browser) back to the client using the redirection URI
- provided earlier (in the request or during client registration). The redirection URI
- includes an authorization code and any local state provided by the client earlier.
- (D) The client makes an access token request from the authorization server's token
- endpoint by including the authorization code received in the previous step. When
- making the request, the client authenticates with the authorization server using
- the client credentials. The client also includes the redirection URI used to obtain
- the authorization code for verification.
- (E) The authorization server authenticates the client. It validates the authorization
- code and ensures that the redirection URI received matches the URI used to redirect
- the client in step (C). If valid, the authorization server responds back with an access
- token and, optionally, a refresh token in case an offline access was requested.
Authorization code request
The authorization code request corresponds to steps (A) and (B) as described in Figure
1. In step (A), the client makes a request to the authorization server in the
application/x-www-form-urlencoded
format, as shown in Listing 1.
Listing 1. Example of an authorization code request
1 2 3 |
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 Host: server.example.com |
The request must contain the following parameters:
- response_type: Required. The value must be set to code.
- client_id: Required. The client ID.
- redirect_uri: Required. Used for user agent redirection.
- scope: Optional. The scope of the access request.
- state: Optional. To maintain the state between the request and callback.
After the request is verified by the authorization server, the server sends an HTTP
redirect code 302 response back to the client. The response will also include a redirection
URI in the http
Location
header. In step (B), the client must redirect the user agent
(typically, a web browser) to this URI. This redirection URI is usually a login page
where the resource owner can sign in with their credentials and grant/revoke access to the client's request.
Authorization code response
The authorization code response is shown in step (C) of Figure 1. If the resource
owner grants the access request, the authorization server issues an authorization
code. The authorization server redirects the user agent to the redirect URI provided
as a part of the request in step (A) and includes the authorization code as a part of
the query component of the redirection URI using the
application/x-www-form-urlencoded
format.
The URI parameters are as follows:
- Code: Required. The authorization code generated by the authorization server.
- The code is temporary and must expire shortly after it was generated. The client
- must not use the authorization code more than once. Any further requests using
- the same code should be revoked by the authorization server. The authorization
- code is bound to the client identifier and the redirection URI.
- State: Required. If the state parameter was present in the client's authorization
- code request, this parameter must be set to the exact value received from the client.
Access token request
This corresponds to step (D) in Figure 1. The client makes a request to the token
endpoint (authorization server) using the
application/x-www-form-urlencoded
format as shown in Listing 2.
Listing 2. Example of an access token request
1 2 3 4 5 6 7 |
POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom&client_id=c342 |
The access token request must have the following parameters set:
- grant_type: Required. The value must be set to authorization_code.
- client_id: Required. The client ID.
- client_secret: Optional. Secret. To authenticate with authorization server.
- code: Required. The authorization code received from the server.
- redirect_uri: Required. Identical to that sent in step (A).
The authorization server verifies that the code and redirect URI are valid. In the case
of confidential clients, the authorization server also authenticates the client using its
client credentials passed in the body of the request or in the authorization header.
Access token response
This corresponds to step (E) in Figure 1. If the access token request is valid and is
authorized, the authorization server returns the access token in an access token
response. An example of a successful response is shown in Listing 3.
Listing 3. Example of a successful access token response
1 2 3 4 5 6 7 8 9 10 11 12 |
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"Bearer", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" } |
If the request is not valid or unauthorized, the authorization server returns an
appropriate error message with code.
Refresh access token request
This is an optional step, which is applicable if the client requested offline access
and was provided a
refresh_token
as a part of the access token request. An access token
is temporary and usually expires after an hour. After the access token expires, the
client would need to repeat the authentication process and the resource owner
would need to log in and provide authorization to enable the client to make the
access token request again.
If the client needs to refresh access tokens while the resource owner is not present
at the browser to log in and authenticate, the client uses the offline access. The
client can request an offline access while making the first authorization code
request (see step (A)). Under this scheme, the authorization server returns a refresh
token in addition to the access token. The refresh token is a long-living token that
does not expire, unless it is explicitly revoked by the resource owner. Every time
the access token expires, the client can use the refresh token to regenerate an
access token without the resource owner needing to sign in and authorize the access request.
The client makes a request to the token endpoint (authorization server) using
the
application/x-www-form-urlencoded
format, as shown in Listing 4:
Listing 4. Request to the token endpoint
1 2 3 4 5 6 |
POST /token HTTP/1.1 Host: server.example.com Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA |
The request parameters are defined as follows:
- grant_type: Required. The value must be set to refresh_token.
- refresh_token: Required. This is retrieved earlier from access token request.
- scope: Optional. The scope of the access request.
The authorization server verifies the refresh token and issues a new access token.
Refresh access token response
If the request is successful, the authorization server returns a new access token.
An example of a successful response is shown in Listing 5.
Listing 5. Refresh access token response
1 2 3 4 5 6 7 8 9 10 11 |
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"Bearer", "expires_in":3600, "example_parameter":"example_value" } |
If the request is not valid or unauthorized, the authorization server returns an
appropriate error message with code.
二、用户评证
三、SAML/JWT
JWT:
SAML:
SAML
的
2
种典型模式
在协议的角度,
SAML
原理非常类似
CAS
和
Kerberos
,
CAS
协议依赖于
CAS Server
,
Kerberos
依赖于
KDC
,而
SAML
则依赖于
Identity Provider
。
根据
Service Provider(
以下简称
SP)
和
Identity Provider(
以下简称
IDP)
的交互方式,
SAML
可以分为以下几种模式:一种是
SP
拉方式,一种是
IDP
推方式。
在
SAML
中,最重要的环节是
SP
如何获取对
Subject
的断言,
SP
拉方式是
SP
主动到
IDP
去了解
Subject
的身份断言,而
IDP
推方式则是
IDP
主动把
Subject
的身份断言通过某种途径告诉
SP
。
2.2.1 SAML
的
POST/Artifact Bindings
方式(即
SP
拉方式)
该方式的主要特点是,
SP
获得客户端的凭证
是
IDP
对
Subject
的一种身份认可
之后,主动请求
IDP
对
Subject
的凭证的断言。如下图所示:
Subject
是根据凭证去访问
SP
的。凭证代表了
Subject
的身份,它类似于“来自
IDP
证明:我就是
Peter
,法国公民”。
现在,让我们看看
SP
拉方式是如何进行的:
Subject
访问
SP
的受保护资源,
SP
发现
Subject
的请求中没有包含任何的授权信息,
于是它重定向用户访问
IDP.
协议执行:
1, Subject
向
IDP
请求凭证
方式是提交用户名
/
密码
2, IDP
通过验证
Subject
提供的信息,来确定是否提供凭证给
Subject
3,
假如
Subject
的验证信息正确,他将获取
IDP
的凭证以及将服务请求同时提交给
SP
。
4, SP
接受到
Subject
的凭证,它是提供服务之前必须验证次凭证,于是,它产生了一个
SAML
请求,要求
IDP
对凭证断言
5,
凭证是
IDP
产生的,它当然知道凭证的内容,于是它回应一个
SAML
断言给
SP
6, SP
信任
IDP
的
SAML
断言,它会根据断言结果确定是否为
Subject
提供服务。
4.2.1 SAML
的
Redirect/POST Bindings
方式
即
IDP
推方式
该方式的主要特点是,
IDP
交给
Subject
的不是凭证,而是断言。
过程如下图所示:
1
,
Subject
访问
SP
的授权服务,
SP
重定向
Subject
到
IDP
获取断言。
2
,
IDP
会要求
Subject
提供能够证明它自己身份的手段
(Password
,
X.509
证书等
3
,
Subject
向
IDP
提供了自己的帐号密码。
4
,
IDP
验证密码之后,会重订向
Subject
到原来的
SP
。
5
,
SP
校验
IDP
的断言
注意,
IDP
会对自己的断言签名,
SP
信任
IDP
的证书,因此,
通过校验签名,能够确信从
Subject
过来的断言确实来自
IDP
的断言
。
6
,如果签名正确,
SP
将向
Subject
提供该服务。
四、Spring + Auth2 + Security
五、Authorization Server Configuration
http://localhost:8080/auth/oauth/authorize?response_type=code&client_id
=acme&redirect_uri=http://example.com
http://localhost:8080/auth/oauth/authorize?username=admin&password=123456&scope=read&grant_type
=password&response_type=code&client_id=acme&redirect_uri=http://example.com
http://localhost:8080/auth/oauth/token?response_type=code&client_id=acme&
redirect_uri=http://example.com&code=SMJ6j2
问题:
{"timestamp":1420442772928,"status":401,"error":"Unauthorized",
"message":"Full authentication is required to access this resource","path":"/resource"}
解决办法:
Authorization:username:password
用户名密码需要通过Request Header传递(key=Authorization,value=base64(username:password))
Application.yml:
server.contextPath: /auth
logging:
level:
org.springframework.security: DEBUG
server:
port: 8080
keystore:
password: mySecretKey
security:
user:
name:admin
password:admin
oauth2:
client:
clientId:acme
clientSecret:acmesecret
authorized-grant-types:authorization_code,refresh_token,password
scope:openid
六、TOKEN 存储方式
(一)InMemoryTokenStore
TOKEN存储方式为默认的配置方式,AuthorizationServerEndpointsConfigurer类如下
private TokenStore tokenStore() {
if (tokenStore == null) {
if (accessTokenConverter() instanceof JwtAccessTokenConverter) {
this.tokenStore = new JwtTokenStore((JwtAccessTokenConverter) accessTokenConverter());
}
else {
this.tokenStore = new InMemoryTokenStore();
}
}
return this.tokenStore;
}
TOKEN生成策略接口(AuthenticationKeyGenerator),从OAuth2Authentication对
象生成唯一TOKEN KEY,基于内存TOKEN,默认生成KEY实现类为:
DefaultAuthenticationKeyGenerator,MD5算法签名
public class DefaultAuthenticationKeyGenerator implements AuthenticationKeyGenerator {
private static final String CLIENT_ID = "client_id";
private static final String SCOPE = "scope";
private static final String USERNAME = "username";
public String extractKey(OAuth2Authentication authentication) {
Map values = new LinkedHashMap();
OAuth2Request authorizationRequest = authentication.getOAuth2Request();
if (!authentication.isClientOnly()) {
values.put(
USERNAME, authentication.getName());
}
values.put(
CLIENT_ID, authorizationRequest.getClientId());
if (authorizationRequest.getScope() != null) {
values.put(
SCOPE, OAuth2Utils.formatParameterList(new TreeSet
(authorizationRequest.getScope())));
}
return
generateKey(values);
}
protected String generateKey(Map values) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
byte[] bytes = digest.digest(values.toString().getBytes("UTF-8"));
return String.format("%032x", new BigInteger(1, bytes));
} catch (NoSuchAlgorithmException nsae) {
throw new IllegalStateException("MD5 algorithm not available. Fatal (should be in the JDK).", nsae);
} catch (UnsupportedEncodingException uee) {
throw new IllegalStateException("UTF-8 encoding not available. Fatal (should be in the JDK).", uee);
}
}
}
可以调用以下方法重写TOKEN生成方式,限于高级用法
public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) {
this.authenticationKeyGenerator = authenticationKeyGenerator;
}
(二)JwtTokenStore
JwtTokenStore实现需要依赖AccessTokenConverter接口,AccessTokenConverter
接口实现类有两个:
1)JWT(header、content、cryto)----JwtAccessTokenConverter接口
2)普通 ------DefaultAccessTokenConverter接口(默认实现)
参考类AuthorizationServerEndpointsConfigurer:
private AccessTokenConverter accessTokenConverter() {
if (this.accessTokenConverter == null) {
accessTokenConverter = new DefaultAccessTokenConverter();
}
return this.accessTokenConverter;
}
private TokenStore tokenStore() {
if (tokenStore == null) {
if (accessTokenConverter() instanceof JwtAccessTokenConverter) {
this.tokenStore = new JwtTokenStore((JwtAccessTokenConverter)
accessTokenConverter());
}
else {
this.tokenStore = new InMemoryTokenStore();
}
}
return this.tokenStore;
}
JWT转换方式:
1)MAC (默认)
采用对用户(字段username)或者客户端信息(oauthrocations 中的AUTHORITIES)进签名,需要依赖
DefaultAccessTokenConverter实现,DefaultAccessTokenConverter又需要
UserAuthenticationConverter接口实现(只有一个实现类DefaultUserAuthenticationConverter),
DefaultUserAuthenticationConverter实现需要依赖UserDetailsService接口获取用户信息
签名属性:
public class
DefaultUserAuthenticationConverter implements UserAuthenticationConverter {
private Collection extends GrantedAuthority> defaultAuthorities;
private
UserDetailsService userDetailsService;
/**
* Optional {@link UserDetailsService} to use when extracting an {@link Authentication}
from the incoming map.
*
* @param userDetailsService the userDetailsService to set
*/
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
/**
* Default value for authorities if an Authentication is being created and the input has
no data for authorities.
* Note that unless this property is set, the default Authentication created by
{@link #extractAuthentication(Map)}
* will be unauthenticated.
*
* @param defaultAuthorities the defaultAuthorities to set. Default null.
*/
public void setDefaultAuthorities(String[] defaultAuthorities) {
this.defaultAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
.arrayToCommaDelimitedString(defaultAuthorities));
}
public Map
convertUserAuthentication(Authentication authentication) {
Map response = new LinkedHashMap();
response.put(USERNAME, authentication.getName());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
public Map convertUserAuthentication(Authentication authentication) {
Map response = new LinkedHashMap();
response.put(USERNAME, authentication.getName());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
签名方法:
// Key随机生成
private String verifierKey = new
RandomValueStringGenerator().generate();
private Signer signer = new
MacSigner(verifierKey);
private String signingKey = verifierKey;
/**
* Get the verification key for the token signatures.
*
* @return the key used to verify tokens
*/
public Map getKey() {
Map result = new LinkedHashMap();
result.put("alg", signer.algorithm());
result.put("value", verifierKey);
return result;
}
public class
MacSigner implements SignerVerifier {
private static final String DEFAULT_ALGORITHM = "HMACSHA256";
public byte[] sign(byte[] bytes) {
try {
Mac mac = Mac.getInstance(algorithm);
mac.init(key);
return mac.doFinal(bytes);
}
catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
}
2)RSA
public void setKeyPair(KeyPair keyPair) {
PrivateKey privateKey = keyPair.getPrivate();
Assert.state(privateKey instanceof RSAPrivateKey, "KeyPair must be an RSA ");
signer = new RsaSigner((RSAPrivateKey) privateKey);
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
verifier = new RsaVerifier(publicKey);
verifierKey = "-----BEGIN PUBLIC KEY-----\n" + new String(Base64.encode(publicKey.getEncoded()))
+ "\n-----END PUBLIC KEY-----";
}
(三)JdbcTokenStore
略
(四)RedisTokenStore
略
AbstractEndpoint
1)AuthorizationEndpoint---/oauth/authorize
2)TokenEndpoint---/oauth/token
3)CheckTokenEndpoint---/oauth/check_token
4)WhitelabelApprovalEndpoint---/oauth/confirm_acces
5)TokenKeyEndpoint---/oauth/token_key
七、Spring Security
一、基础配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired MyUserDetailsService detailsService;
@Override protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() .and().formLogin().loginPage("/login").permitAll()
.defaultSuccessUrl("/", true) .and().logout().logoutUrl("/logout")
.and().sessionManagement().maximumSessions(1).expiredUrl("/expired")
.and() .and().exceptionHandling().accessDeniedPage("/accessDenied");
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/js/**", "/css/**", "/images/**", "/**/favicon.ico");
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(detailsService).passwordEncoder(new BCryptPasswordEncoder());
}
}
1.@EnableWebSecurity: 禁用Boot的默认Security配置,配合@Configuration启用自定义配置
(需要扩展WebSecurityConfigurerAdapter)
2.@EnableGlobalMethodSecurity(prePostEnabled = true): 启用
Security注解
,
例如最常用的@PreAuthorize
3.configure(HttpSecurity): Request层面的配置,对应XML Configuration中的元素
4.configure(WebSecurity):
Web层面的配置,一般用来配置无需安全检查的路径
5.configure(AuthenticationManagerBuilder): 身份验证配置,用于注入
自定义身份验证Bean
和
密码校验规则
二、扩展配置
完成基础配置之后,下一步就是实现自己的UserDetailsService和PermissionEvaluator,
分别用于自定义Principle, Authority和Permission。
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired private LoginService loginService;
@Autowired private RoleService roleService;
@Override
public UserDetails loadUserByUsername(String username) {
if (StringUtils.isBlank(username)) {
throw new UsernameNotFoundException("用户名为空");
}
Login login = loginService.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
Set authorities = new HashSet<>();
roleService.getRoles(login.getId()).forEach(r -> authorities.add(new SimpleGrantedAuthority(r.getName())));
return new org.springframework.security.core.userdetails.User( username, login.getPassword(),
true,//是否可用 true,//是否过期 true,//证书不过期为true true,//账户未锁定为true authorities);
}
}
创建GrantedAuthority对象时,一般名称加上ROLE_前缀。
@Component
public class MyPermissionEvaluator implements PermissionEvaluator {
@Autowired private LoginService loginService;
@Autowired private RoleService roleService;
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
String username = authentication.getName();
Login login = loginService.findByUsername(username).get();
return roleService.authorized(login.getId(), targetDomainObject.toString(), permission.toString());
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId,
String targetType, Object permission) {
// not supported return false;
}
}
1. hasPermission(Authentication, Object, Object)和hasPermission(Authentication,
Serializable, String, Object)
两个方法分别对应Spring Security中两个同名的表达式。
八、授权页修改
1) 修改管理端点URL
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
MyAuthorizationCodeService authorizationCodeServices = authorizationCodeServices();
authorizationCodeServices.setClientDetailsService(endpoints.getClientDetailsService());
endpoints.tokenStore(tokenStore())
.tokenEnhancer(jwtTokenEnhancer())
.authorizationCodeServices(authorizationCodeServices)
.authenticationManager(authenticationManager)
.pathMapping("/oauth/authorize", "/oauth2/authorize")
.pathMapping("/oauth/token", "/oauth2/token")
.pathMapping("/oauth/check_token", "/oauth2/check_token")
.pathMapping("/oauth/confirm_access", "/oauth2/confirm_access")
.pathMapping("/oauth/token_key", "/oauth2/token_key")
.pathMapping("/oauth/error", "/oauth2/error");
}
2)但是授权确认页面表单写死Action
端点类:WhitelabelApprovalEndpoint
@FrameworkEndpoint
@SessionAttributes("authorizationRequest")
public class WhitelabelApprovalEndpoint {
@RequestMapping("/oauth/confirm_access")
public ModelAndView getAccessConfirmation(Map model,
HttpServletRequest request) throws Exception {
String template = createTemplate(model, request);
if (request.getAttribute("_csrf") != null) {
model.put("_csrf", request.getAttribute("_csrf"));
}
return new ModelAndView(new SpelView(template), model);
}
protected String createTemplate(Map model, HttpServletRequest request) {
String template = TEMPLATE;
if (model.containsKey("scopes") || request.getAttribute("scopes") != null) {
template = template.replace("%scopes%", createScopes(model, request)).replace("%denial%", "");
}
else {
template = template.replace("%scopes%", "").replace("%denial%", DENIAL);
}
if (model.containsKey("_csrf") || request.getAttribute("_csrf") != null) {
template = template.replace("%csrf%", CSRF);
}
else {
template = template.replace("%csrf%", "");
}
return template;
}
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" : "";
String value = SCOPE.replace("%scope%", scope).replace("%key%", scope).replace("%approved%", approved)
.replace("%denied%", denied);
builder.append(value);
}
builder.append("");
return builder.toString();
}
private static String CSRF = "";
private static String DENIAL = "