OAuth2 统一认证
原理
OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。"客户端"不能直接登录"服务提供商",只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。
A、用户打开客户端以后,客户端要求用户给予授权。
B、用户同意给予客户端授权。
C、客户端使用上一步获得的授权,向认证服务器申请令牌。
D、认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
E、客户端使用令牌,向资源服务器申请获取资源。
F、资源服务器确认令牌无误,同意向客户端开放资源。
客户端授权模式
上面 B、流程中是用户给予客户端授权。oauth2 定义了下面四种授权方式:
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
授权码模式
A、用户访问客户端,后者将前者导向认证服务器。
B、用户选择是否给予客户端授权。
C、假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
D、客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
E、认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
步骤说明:
① A步骤中,客户端申请认证的URI,包含以下参数:
* response_type:表示授权类型,必选项,此处的值固定为"code"
* client_id:表示客户端的ID,必选项
* redirect_uri:表示重定向URI,可选项
* scope:表示申请的权限范围,可选项
* state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1Host: server.example.com
② C步骤中,服务器回应客户端的URI,包含以下参数:
* code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
* state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
HTTP/1.1 302 FoundLocation: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
&state=xyz
③ D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:
* grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。
* code:表示上一步获得的授权码,必选项。
* redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
* client_id:表示客户端ID,必选项。
POST /token HTTP/1.1Host: server.example.comAuthorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JWContent-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
④ E步骤中,认证服务器发送的HTTP回复,包含以下参数:
* access_token:表示访问令牌,必选项。
* token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
* expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
* refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
* scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
密码模式
密码模式是将授权码模式中的授权码固定为用户名和密码。
A、用户向客户端提供用户名和密码。
B、客户端将用户名和密码发给认证服务器,向后者请求令牌。
C、认证服务器确认无误后,向客户端提供访问令牌。
步骤说明:
① B步骤中,客户端发出的HTTP请求,包含以下参数:
* grant_type:表示授权类型,此处的值固定为"password",必选项。
* username:表示用户名,必选项。
* password:表示用户的密码,必选项。
* scope:表示权限范围,可选项。
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=johndoe&password=A3ddj3w
② C步骤中,认证服务器向客户端发送访问令牌,下面是一个例子。
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"example",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
"example_parameter":"example_value"
}
注意,整个过程中,客户端不得保存用户的密码。
项目实践
1 .pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
2 .SecurityConfig.java(主要配置文件)
package club.lemos.sso.config;
import club.lemos.sso.config.security.ClientResources;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.filter.CompositeFilter;
import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.List;
@Configuration
@EnableOAuth2Client
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private OAuth2ClientContext oauth2ClientContext;
private final UserDetailsService userDetailService;
@Autowired
public SecurityConfig(UserDetailsService userDetailService) {
this.userDetailService = userDetailService;
}
/**
* 详细的路由配置参数
*
* @param http 配置
* @throws Exception 相关异常
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors() // 跨域支持
.and()
.antMatcher("/**") // 捕捉所有路由
.authorizeRequests()
.antMatchers("/", "/login**", "/webjars/**", "/github").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")) // 认证入口(跳转)
.and()
.formLogin().loginProcessingUrl("/doLogin") // 表单请求的路由为 "POST /login"
.defaultSuccessUrl("/").failureUrl("/login?err=1")
.permitAll()
.and()
.logout().logoutUrl("/logout") // 注销请求的路由为 "GET /logout"
.logoutSuccessUrl("/")
.permitAll()
.invalidateHttpSession(true)
.clearAuthentication(true)
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // csrf 安全处理
.and()
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class); // 第三方授权层
}
@Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(
OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
private Filter ssoFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
filters.add(ssoFilter(github(), "/login/github"));
filter.setFilters(filters);
return filter;
}
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);
OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
filter.setRestTemplate(template);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(
client.getResource().getUserInfoUri(), client.getClient().getClientId());
tokenServices.setRestTemplate(template);
filter.setTokenServices(tokenServices);
return filter;
}
/**
* github 授权连接
*
* @return 第三方授权连接对象
*/
@Bean
@ConfigurationProperties("github")
public ClientResources github() {
return new ClientResources();
}
/**
* BCrypt 密码加密
*
* @return BCrypt 编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailService);
// authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
/**
* 用户认证服务(用户名+密码)
*
* @param auth 认证
* @throws Exception 相关异常
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider());
}
// TODO 提供一个访问用户信息(昵称,角色信息等等)的 api
// TODO 提供 cookie持久化时间
// TODO 注销功能实现
}
ClientResources.java
package club.lemos.sso.config.security;
import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
public class ClientResources {
@NestedConfigurationProperty
private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();
@NestedConfigurationProperty
private ResourceServerProperties resource = new ResourceServerProperties();
public AuthorizationCodeResourceDetails getClient() {
return client;
}
public ResourceServerProperties getResource() {
return resource;
}
}
3 .application.yml(配置文件)
logging:
level:
org:
springframework:
security: DEBUG
root: INFO
server:
port: 8080
spring:
datasource:
dbcp2:
initial-size: 10
max-idle: 8
min-idle: 8
driverClassName: com.mysql.jdbc.Driver
password: root
url: jdbc:mysql://localhost:3306/sso?useSSL=false
username: root
freemarker:
charset: UTF-8
check-template-location: true
content-type: text/html
expose-request-attributes: true
expose-session-attributes: true
request-context-attribute: request
thymeleaf:
cache: false
prefix: classpath:/templates/
suffix: .html
github:
client:
clientId: dd2bf79a9e6be256f0e8
clientSecret: 0e555a2ee5d627e3abdee3f5096de6d8278d3413
accessTokenUri: https://github.com/login/oauth/access_token
userAuthorizationUri: https://github.com/login/oauth/authorize
clientAuthenticationScheme: form
resource:
userInfoUri: https://api.github.com/user
4 .UserDetailsServiceImpl.java(用户接口实现)
package club.lemos.sso.config.security;
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.Service;
import java.util.Arrays;
import java.util.Date;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO 从数据库中查找用户
return new UserDetailsImpl(1L, "lisi", "password",
Arrays.asList(new SimpleGrantedAuthority("USER"), new SimpleGrantedAuthority("ADMIN")), true, new Date());
}
}
UserDetailsImpl.java
package club.lemos.sso.config.security;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Date;
public class UserDetailsImpl implements UserDetails {
private final Long id;
private final String username;
private final String password;
private final Collection<? extends GrantedAuthority> authorities;
private final boolean enabled;
private final Date lastPasswordResetDate;
public UserDetailsImpl(Long id, String username, String password, Collection<? extends GrantedAuthority> authorities, boolean enabled, Date lastPasswordResetDate) {
this.id = id;
this.username = username;
this.password = password;
this.authorities = authorities;
this.enabled = enabled;
this.lastPasswordResetDate = lastPasswordResetDate;
}
@JsonIgnore
public Long getId() {
return id;
}
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public boolean isEnabled() {
return enabled;
}
@JsonIgnore
public Date getLastPasswordResetDate() {
return lastPasswordResetDate;
}
}
相关问题
问题一、提供oAuth2 接口 及 token 的获取
之前的Web端(B\S结构),可以正常通信。登录跳转什么的。对于受限资源,需要通过 oauth2授权( C\S 结构)。可以通过Web端发送一个请求进行认证。认证成功,获得 token,可以使用 token访问f服务器受限资源。formLogin 即表单登录,比较好理解。oauth2 登录,需要先获得 token,再用它访问 api 资源服务器,获取信息。
1. 证书 token
curl client:secret@localhost:8090/oauth/token -d grant_type=client_credentials
2. 密码 token
当应用启动时,springboot 会创建一个默认的用户,用户id为‘user‘。密码是随机的,但可以从打印的日志中看到。
curl client:secret@localhost:8090/oauth/token -d grant_type=password -d username=user -d password=...
或者使用定义好的用户名及密码进行认证
curl client:secret@localhost:8090/oauth/token -d grant_type=password -d username=admin -d password=admin
认证后,访问资源服务
curl http://localhost:8090/api/users -H "Authorization: bearer 7e7b7ced-3747-43a2-8134-c7e6b87c6451"
3. 页面中,可以通过发送带 auth 认证头的请求,访问oauth服务器
ajax 请求认证:
$.ajax({
type: "GET",
url: "index1.php",
dataType: 'json',
async: false,
headers: {
"Authorization": "Basic " + btoa(USERNAME + ":" + PASSWORD)
},
data: '{ "comment" }',
success: function (){
alert('Thanks for your comment!');
}});
问题二、github 第三方授权接入(原文:https://developer.github.com/v3/oauth/)
从 github上申请 开发权限:
执行流程
1> 点击页面的 callback超链接(比如 go to github)。
callbackURL = http://localhost:8090/login/github GET
2> 重定向到授权页面,用户点击授权
user-authorization-uri = https://github.com/login/oauth/authorize?client_id=141c0a61de83cf2d9841&redirect_uri=http://localhost:8090/login/github&response_type=code&scope=user&state=4jgVT2
3> 重定向回自己的页面,并携带一个 code (授权码) 和 前一步中的 state参数,如果 states 匹配,则可以发送一个 POST https://github.com/login/oauth/access_token
http://localhost:8090/login/github?code=2c6dcdce82ef1473e148&state=4jgVT2
4> 请求 token(授权成功,会自动发送这个请求)
https://github.com/login/oauth/access_token POST
响应 token(包含着授权信息,存储在 JSESSION 中)
access_token=e72e16c7e42f292c6912e7710c838347ae178b4a&scope=user%2Cgist&token_type=bearer
或者
Accept: application/json {"access_token":"e72e16c7e42f292c6912e7710c838347ae178b4a", "scope":"repo,gist", "token_type":"bearer"}
5> 访问 Github API(使用 js访问)
GET https://api.github.com/user?access_token=...
或者设置头信息
Authorization: token OAUTH-TOKEN
问题三、 github 的token的获取
使用 RestTemplate访问。
OAuth2RestTemplate template = oAuth2RestTemplate(new AuthorizationCodeResourceDetails());
template.setRetryBadAccessTokens(false);
token = template.getAccessToken();
或者在配置文件中从上下文中直接获取。
OAuth2AccessToken accessToken = oauth2ClientContext.getAccessToken();
完整项目下载
完整项目下载—— 点我
其他
参阅文档
springboot 官方文档
Spring-Boot-Reference-Guide
https://qbgbook.gitbooks.io/spring-boot-reference-guide-zh/content/
Spring Boot and OAuth2 *****接入github 的详细配置******
https://spring.io/guides/tutorials/spring-boot-oauth2/
---------------------------------------------------------------------------
有用的文章
spring security & oauth2
http://www.jianshu.com/p/6b211e845b16/
spring-security-oauth2 server
http://www.jianshu.com/p/028043425b09
详解Spring Security进阶身份认证之UserDetailsService(附源码)
http://favccxx.blog.51cto.com/2890523/1609692
---------------------------------------------------------------------------
github 相关文档
https://developer.github.com/v3/
https://developer.github.com/v3/oauth/
https://developer.github.com/v3/oauth_authorizations/#list-your-authorizations
https://help.github.com/articles/connecting-with-third-party-applications/