[最近工作需要,了解了一些OAuth2,并进行了实际操作,网上资料比较乱,这里总结一下,本地一共记录了6篇,这里做适当删减,便于理解,更新会有点慢!]
[完整代码我会上传到我的github,点击直达,有需要的可以自行参考!]
OAuth2
OAuth2.0 是目前最流行的授权机制,用来授权第三方应用,获取用户数据,是一种协议规范。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该令牌在限定时间、限定范围访问指定资源。主要涉及的RFC规范有RFC6749(整体授权框架),RFC6750(令牌使用),RFC6819(威胁模型)这几个,一般我们需要了解的就是RFC6749。
[文字都有,参考了其他人的]
RFC6749 重要概念
- resource owner: 拥有被访问资源的用户
- user-agent: 一般来说就是浏览器
- client: 第三方应用
- Authorization server: 认证服务器,用来进行用户认证并颁发token
- Resource server:资源服务器,拥有被访问资源的服务器,需要通过token来确定是否有权限访问
授权方式
- 授权码(authorization-code)
- 隐藏式(implicit)
- 密码式(password):
- 客户端凭证(client credentials)
注意,不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。 更多的请参考: 阮一峰 这里介绍一下我们要搭建的,即授权码方式,也是目前应用最广泛的,一些网站可以支持QQ、微信等三方登录,也是使用的此方式。
授权码流程图 总结一下 大致流程
- 携带 client_id,redirect_uri 去请求 OAuth 的 oauth/authorize 服务,登录后获取授权码
- 后台使用获得的授权码,配合 client_id,client_secret 去请求 OAuth 的获取 oauth/token 服务,获取到 access_token 凭证
- 使用获取到的 access_token 去请求资源服务器获取相关数据。
快速入门 一、搭建 Authorization Server 1.创建 auth-server 工程,引入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>${oauth2.version}</version>
</dependency>
配置 application.yaml
server:
port: 7777
spring:
messages:
basename: i18n/messages
encoding: UTF-8
thymeleaf:
cache: false # 开发时关闭缓存,不然没法看到实时页面
mode: HTML # 用非严格的 HTML
encoding: UTF-8
servlet:
content-type: text/html
2. 配置 SecurityConfiguration 继承 WebSecurityConfigurerAdapter
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置认证信息
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在内存中创建2个用户,便于测试,后期可改为数据库中获取
auth.inMemoryAuthentication()
.passwordEncoder(encoder())
.withUser("user").password(encoder().encode("user")).roles("USER")
.and()
.withUser("admin").password(encoder().encode("admin")).roles("ADMIN");
}
/**
* 配置核心过滤器
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/asserts/**");
}
/**
* 配置Security的认证策略
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置登录页并允许访问
http.formLogin()
.loginPage("/login")
.permitAll()
.and()
// 配置登出页面
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.and()
.authorizeRequests()
.antMatchers("/oauth/**", "/login/**", "/logout/**")
.permitAll()
// 其余所有请求全部需要鉴权认证
.anyRequest()
.authenticated()
.and()
// 关闭跨域保护,Basic登录
.csrf().disable()
.httpBasic().disable();
}
}
3.配置 OAuth2Configuration 继承 AuthorizationServerConfigurerAdapter
/**
*
* describe OAuth2 配置 - TOKEN 保存在内存中
* @author 20018704
* @date 2020/9/2 10:39
*/
@Configuration
@EnableAuthorizationServer
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {
private static final String CLIENT_ID = "cms";
private static final String SECRET_CHAR_SEQUENCE = "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 = 2 * 60;
private static final int REFRESH_TOKEN_VALIDITY_SECONDS = 2 * 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";
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
/**
* 用来配置令牌端点(Token Endpoint)的安全约束
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()") // 开启/oauth/token_key验证端口认证权限访问
.checkTokenAccess("permitAll()") // 开启/oauth/check_token验证端口认证权限访问
.allowFormAuthenticationForClients(); // 允许表单认证否则在授权码模式下会导致无法根据code获取token
}
/**
* 用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化
* 你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 使用内存存储
clients.inMemory()
.withClient(CLIENT_ID) //标记客户端id
.secret(passwordEncoder.encode(SECRET_CHAR_SEQUENCE)) //客户端安全码
.autoApprove(true) //为true 直接自动授权成功返回code
//重定向uri,uri中携带的必须为这里面填写的之一,比如可以在下面的接口中,处理返回的code
.redirectUris("http://127.0.0.1:8084/cms/thirdLogin")
.scopes(ALL) //允许授权范围
.authorizedGrantTypes(GRANT_TYPE_PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT); //允许授权类型
}
/**
* 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 使用内存保存生成的token
endpoints.authenticationManager(authenticationManager).tokenStore(memoryTokenStore());
}
public TokenStore memoryTokenStore() {
// 最基本的InMemoryTokenStore生成token
return new InMemoryTokenStore();
}
}
4.自定义登录页面
添加 Login 方法
@Controller
public class LoginController {
@GetMapping(value = {"/login"})
public ModelAndView toLogin(){
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("login");
return modelAndView;
}
}
使用 thymeleaf 模板引擎来构建 login 页面,相关国际化配置不在这里展示。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" />
<meta name="author" content="" />
<title>Signin Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="../static/asserts/css/bootstrap.min.css" target="_blank" rel="external nofollow" th:href="@{asserts/css/bootstrap.min.css}" target="_blank" rel="external nofollow" rel="stylesheet" />
<!-- Custom styles for this template -->
<link href="../static/asserts/css/signin.css" target="_blank" rel="external nofollow" th:href="@{asserts/css/signin.css}" target="_blank" rel="external nofollow" rel="stylesheet"/>
</head>
<body class="text-center">
<form class="form-signin" th:action="@{/login}" method="post">
<img class="mb-4" th:src="@{asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72" />
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Oauth2.0 Login</h1>
<label class="sr-only" th:text="#{login.username}">Username</label>
<input type="text" class="form-control" name="username" th:placeholder="#{login.username}" required="" autofocus="" value="nicky" />
<label class="sr-only" th:text="#{login.password} ">Password</label>
<input type="password" class="form-control" name="password" th:placeholder="#{login.password}" required="" value="123" />
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember me" th:text="#{login.remember}" />
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btnName}">Sign in</button>
<p class="mt-5 mb-3 text-muted">© 2019</p>
<a class="btn btn-sm" th:href="@{/login()} " target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" >中文</a>
<a class="btn btn-sm" th:href="@{/login()} " target="_blank" rel="external nofollow" target="_blank" rel="external nofollow" >English</a>
</form>
</body>
</html>
整个认证服务器完整结构
5.启动认证服务器,进行测试
a.访问默认授权端点 /oauth/authorize ,获取授权码
localhost:7777/oauth/authorize?client_id=cms&response_type=code 注意,若后面携带了redirect_uri,则必须与配置的redirectUris完全一致,这里是 http://127.0.0.1:8084/cms/thirdLogin,
localhost:7777/oauth/authorize?client_id=cms&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A8084%2Fcms%3F%2FthirdLogin%3FauthType%3Dtest
如果携带的redirect_uri不一致,则会提示invalid_grant错误:
b.返回到登录页面,进行登录
c.登录成功,获取到 code
d.postman模拟后端访问令牌端点 /oauth/token,获取 access_token
e.访问令牌解析端点,模拟校验资源服务令牌解析
二、搭建 Resource Server(非Client端) 1.创建 spring boot 工程,引入依赖
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>
<oauth2.version>2.2.4.RELEASE</oauth2.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>${oauth2.version}</version>
</dependency>
</dependencies>
配置 application.yaml 文件,声明 check_token
security:
oauth2:
resource:
token-info-uri: http://localhost:7777/oauth/check_token
user-info-uri: http://localhost:7777/user
server:
port: 7000
2.配置 ResourceServerConfiguration 继承 ResourceServerConfigurerAdapter
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint()) // 匿名用户访问无权限资源时的异常
.accessDeniedHandler(new CustomAccessDeineHandler()) // 认证过的用户访问无权限资源时的异常
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.csrf().disable().httpBasic();
}
}
编写两个异常处理类
/**
* 用来解决认证过的用户访问无权限资源时的异常
* AccessDeniedHandler 默认实现是 AccessDeniedHandlerImpl。该类对异常的处理是返回403错误码。
*/
public class CustomAccessDeineHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setCharacterEncoding("utf-8");
response.setContentType("text/javascript;charset=utf-8");
response.getWriter().print("没有访问权限!");
}
}
/**
* AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常
* AuthenticationEntryPoint 默认实现是 LoginUrlAuthenticationEntryPoint, 该类的处理是转发或重定向到登录页面
*/
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setCharacterEncoding("utf-8");
response.setContentType("text/javascript;charset=utf-8");
response.getWriter().print("您还未进行登录,没有访问权限!");
}
}
3.编写用户 Controller 资源接口
@RestController
public class UserController {
@GetMapping("/get_resource")
public String getResource() {
return "OK";
}
@GetMapping("/user")
public User getUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String name = authentication.getName();
User user = new User();
user.setName(name);
return user;
}
@GetMapping("/me")
public User getMe(Authentication authentication) {
User user = new User();
user.setName(authentication.getName());
return user;
}
}
4.启动 Resource Server
4.1直接访问资源路径,提示资源未认证
(未配置AuthenticationEntryPoint,出现上面的内容,否则是下面的内容!)
4.2携带上面postman获取的 access_token 访问,成功访问到受限资源