Oauth协议为用户资源的授权提供了一个安全的、开放而又建议的标准。oauth的授权不会是第三方初级到用户的账号信息(如用户名与密码),及第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此oauth是安全的。oauth是Open Authorization的简写
(1). 比如说我和朋友去银行,银行先后验证我们俩的身份证。银行让我签一个授权合同,上面写着我授权朋友查我的账户,银行给我和朋友各一份合同凭证。银行后台登记朋友拿这个凭证可以查我的账户这样一条信息。之后朋友拿着凭证和他自己的身份证,果然查到了我的账户信息
(2). 还有就是自己公司需要提供接口给别的公司使用,由于是外网环境,所以需要有一套安全机制保障,这个时候oauth2就可以作为一个方案
网上关于Oauth 2.0 的概念挺多的,建议大家去看下阮一峰的文章,很好理解
配置资源服务
配置认证服务
配置spring security
我在前面已经讲过spring security的文章, spring security oauth2是建立在spring security基础之上的,所以有一些体系是公用的, 前两个是oauth2的重点
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
不难看出来,上面六个步骤之中,B是关键,即用户怎样才能给于客户端授权。有了这个授权以后,客户端就可以获取令牌,进而凭令牌获取资源
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。
授权码模式(authorization code)
简化模式(implicit)
密码模式(resource owner password credentials)
客户端模式(client credentials)
本文重点讲解接口对接中常使用的密码模式(以下简称password模式)和客户端模式(以下简称client模式),授权码模式使用到了回调地址,是最为复杂的方式,通常网站中经常出现的微博,qq第三方登录,都会采用这个形式。简化模式不常用
我是基于微服务进行开发的 所以使用Spring Cloud作为微服务框架实现服务注册发现
系统结构
eureka-server 服务注册
oauth-server 认证服务
服务注册先不说 不了解的也没事 直接下载源码启动就好了 重点说下oauth2 后面会出Spring Cloud教程
<!--cloud客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--MySQL驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--Mybatis-Plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.0.6</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.6</version>
</dependency>
<!-- 模板引擎 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
spring-cloud-starter-oauth2是对spring-cloud-starter-security、spring-security-oauth2、spring-security-jwt这3个依赖的整合
既然是对接口的安全保障 那么我们先暴露出一个接口
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
return "product id : " + id;
}
@GetMapping("/order/{id}")
public String getOrder(@PathVariable String id) {
//for debug
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
log.info("authentication: " + authentication.getAuthorities().toString());
return "order id : " + id;
暴露一个商品查询接口,后续不做安全限制,一个订单查询接口,后续添加访问控制
这两个都是oauth2的核心配置,为了更好理解我都放在一个工程里面,后面在进行拆分
package com.li.oauthserver.config;
import com.li.oauthserver.handler.CustomTokenEnhancer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.*;
import java.util.concurrent.TimeUnit;
/**
* @Classname AuthorizationServerConfiguration
* @Description 授权服务器
* @Author 李号东 lihaodongmail@163.com
* @Date 2019-03-18 22:52
* @Version 1.0
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
CustomTokenEnhancer customTokenEnhancer;
private static final String DEMO_RESOURCE_ID = "order";
// 我对于两种模式的理解便是,如果你的系统已经有了一套用户体系,每个用户也有了一定的权限,可以采用password模式
// 如果仅仅是接口的对接,不考虑用户,则可以使用client模式
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// password 持多种编码,通过密码的前缀区分编码方式
String finalSecret = "{bcrypt}" + new BCryptPasswordEncoder().encode("123456");
//配置两个客户端,一个用于password认证一个用于client认证
//client模式,没有用户的概念,直接与认证服务器交互,用配置中的客户端信息去申请accessToken,
// 客户端有自己的client_id,client_secret对应于用户的username,password,而客户端也拥有自己的authorities,
// 当采取client模式认证时,对应的权限也就是客户端自己的authorities
clients.inMemory().withClient("client_1")
.resourceIds(DEMO_RESOURCE_ID)
.authorizedGrantTypes("client_credentials", "refresh_token")
.scopes("select")
.authorities("ROLE_ADMIN")
.secret(finalSecret)
//password模式,自己本身有一套用户体系,在认证时需要带上自己的用户名和密码,以及客户端的client_id,client_secret
// 此时,accessToken所包含的权限是用户本身的权限,而不是客户端的权限
.and().withClient("client_2")
.resourceIds(DEMO_RESOURCE_ID)
.authorizedGrantTypes("password", "refresh_token")
.scopes("select")
.secret(finalSecret);
}
@Bean
public TokenStore tokenStore() {
// return new JdbcTokenStore(dataSource);
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer
// 允许客户表单认证,不加的话/oauth/token无法访问
.allowFormAuthenticationForClients()
// 对于CheckEndpoint控制器[框架自带的校验]的/oauth/token端点允许所有客户端发送器请求而不会被Spring-security拦截
// 开启/oauth/token_key验证端口无权限访问
.tokenKeyAccess("permitAll()")
// 要访问/oauth/check_token必须设置为permitAll(),但这样所有人都可以访问了,设为isAuthenticated()又导致访问不了,这个问题暂时没找到解决方案
// 开启/oauth/check_token验证端口认证权限访问
.checkTokenAccess("permitAll()");
}
//定义授权和令牌端点以及令牌服务
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// redisTokenStore
// endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
// .authenticationManager(authenticationManager)
// .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
endpoints
//指定token存储位置
.tokenStore(tokenStore())
// 配置JwtAccessToken转换器
.tokenEnhancer(accessTokenConverter())
//指定认证管理器
.authenticationManager(authenticationManager)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
;
// 配置tokenServices参数
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
// 是否支持刷新
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
// 20分钟
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.MINUTES.toSeconds(20));
endpoints.tokenServices(tokenServices);
}
}
简单说下spring security oauth2的认证思路
client模式
,没有用户的概念,直接与认证服务器交互,用配置中的客户端信息去申请accessToken,客户端有自己的clientid,clientsecret对应于用户的username,password,而客户端也拥有自己的authorities,当采取client模式认证时,对应的权限也就是客户端自己的authorities password模式
,自己本身有一套用户体系,在认证时需要带上自己的用户名和密码,以及客户端的clientid,clientsecret。此时,accessToken所包含的权限是用户本身的权限,而不是客户端的权限 你的系统已经有了一套用户体系,每个用户也有了一定的权限,可以采用password模式;如果仅仅是接口的对接,不考虑用户,则可以使用client模式
其中我们将配置授权服务器以使用 JwtTokenStore
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}
//定义授权和令牌端点以及令牌服务
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
//指定token存储位置
.tokenStore(tokenStore())
// 配置JwtAccessToken转换器
.tokenEnhancer(accessTokenConverter())
//指定认证管理器
.authenticationManager(authenticationManager)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
;
// 配置tokenServices参数
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
// 是否支持刷新
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
// 1分钟
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.MINUTES.toSeconds(20));
endpoints.tokenServices(tokenServices);
}
请注意,我在 JwtAccessTokenConverter 中使用了一个对称密钥来签署我们的令牌 — 这意味着我们还需要对资源服务器使用同样的密钥
package com.li.oauthserver.config;
import com.li.oauthserver.handler.AuthExceptionEntryPoint;
import com.li.oauthserver.handler.CustomAccessDeniedHandler;
import com.li.oauthserver.handler.CustomTokenEnhancer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import java.util.Arrays;
/**
* @Classname ResourceServerConfiguration
* @Description 资源服务器
* @Author 李号东 lihaodongmail@163.com
* @Date 2019-03-18 22:51
* @Version 1.0
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
private static final String DEMO_RESOURCE_ID = "order";
@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;
@Autowired
private AuthExceptionEntryPoint authExceptionEntryPoint;
@Autowired
CustomTokenEnhancer customTokenEnhancer;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.resourceId(DEMO_RESOURCE_ID).stateless(true)
.authenticationEntryPoint(authExceptionEntryPoint) // 外部定义的token错误进入的方法
.accessDeniedHandler(accessDeniedHandler); // 没有权限的进入方法
resources.tokenServices(tokenServices());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeRequests()
// 配置order访问控制,必须认证后才可以访问
.antMatchers("/order/**").authenticated();
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}
@Bean
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
}
主要是对服务资源进行安全控制 在刚开始定义订单查询接口, 在这里进行安全控制访问
同样使用JWT的一些配置
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("123");
return converter;
}
@Bean
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
请记住,我们将这两个服务器定义为完全分离且可独立部署。 这就是我们为何需要在新配置中再次声明一些相同的 bean 的原因
配置spring security
和上篇的配置复用地方很多
package com.li.oauthserver.security;
import com.li.oauthserver.model.Role;
import com.li.oauthserver.model.User;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* @Author 李号东
* @Description 用户身份权限认证类 登陆身份认证
* @Date 13:29 2019-03-16
* @Param
* @return
**/
@Setter
@Getter
@ToString
public class SecurityUser implements UserDetails {
private static final long serialVersionUID = 1L;
private Integer id;
private String username;
private String password;
private List<Role> role;
private Date lastPasswordResetDate;
public SecurityUser(Integer id, String username, String password, List<Role> role ) {
this.id = id;
this.username = username;
this.password = password;
this.role = role;
}
public SecurityUser(String username, String password, List<Role> role) {
this.username = username;
this.password = password;
this.role = role;
}
public SecurityUser(Integer id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
//返回分配给用户的角色列表
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : role) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
//账户是否未过期,过期无法验证
@Override
public boolean isAccountNonExpired() {
return true;
}
//指定用户是否解锁,锁定的用户无法进行身份验证
@Override
public boolean isAccountNonLocked() {
return true;
}
//指示是否已过期的用户的凭据(密码),过期的凭据防止认证
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//是否可用 ,禁用的用户不能身份验证
@Override
public boolean isEnabled() {
return true;
}
}
package com.li.oauthserver.config;
import com.li.oauthserver.security.UserServiceDetail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @Classname SecurityConfiguration
* @Description TODO
* @Author 李号东 lihaodongmail@163.com
* @Date 2019-03-18 22:29
* @Version 1.0
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceDetail userServiceDetail;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.requestMatchers().anyRequest()
.and()
.authorizeRequests()
.anyRequest().authenticated()
.antMatchers("/oauth/**").permitAll();
}
/**
* password 支持多种编码,通过密码的前缀区分编码方式,推荐
*/
@Bean
PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
/**
* 这一步的配置是必不可少的,否则SpringBoot会自动配置一个AuthenticationManager,覆盖掉内存中的用户
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
@Autowired
public void config(AuthenticationManagerBuilder auth) throws Exception {
//设置UserDetailsService以及密码规则
auth.userDetailsService(userServiceDetail).passwordEncoder(passwordEncoder());
}
}
容器中的UserDetailsService,我自定义的实现的;后者是替换了AuthenticationManager,当然你还会在SecurityConfiguration 复写其他配置,这么配置最终会由一个委托者去认证。如果你熟悉spring security,会知道AuthenticationManager和AuthenticationProvider以及UserDetailsService的关系,他们都是顶级的接口,实现类之间错综复杂的聚合关系…配置方式千差万别,但理解清楚认证流程,知道各个实现类对应的职责才是掌握spring security的关键
测试controller层
package com.li.oauthserver.controller;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.li.oauthserver.model.User;
import com.li.oauthserver.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
/**
* @Classname TestEndpoints
* @Description TODO
* @Author 李号东 lihaodongmail@163.com
* @Date 2019-03-18 22:26
* @Version 1.0
*/
@Slf4j
@RestController
public class TestEndpoints {
@Autowired
private IUserService userService;
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
return "product id : " + id;
}
@GetMapping("/order/{id}")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String getOrder(@PathVariable String id) {
//for debug
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
log.info("authentication: " + authentication.getAuthorities().toString());
return "order id : " + id;
}
@GetMapping("/getPrinciple")
public OAuth2Authentication getPrinciple(OAuth2Authentication oAuth2Authentication, Principal principal, Authentication authentication) {
log.info(oAuth2Authentication.getUserAuthentication().getAuthorities().toString());
log.info(oAuth2Authentication.toString());
log.info("principal.toString() " + principal.toString());
log.info("principal.getName() " + principal.getName());
log.info("authentication: " + authentication.getAuthorities().toString());
return oAuth2Authentication;
}
@RequestMapping(value = "/registry", method = RequestMethod.POST)
public User createUser(@RequestParam("username") String username, @RequestParam("password") String password) {
if (StringUtils.isNotEmpty(username) && StringUtils.isNotEmpty(password)) {
return userService.create(username, password);
}
return null;
}
}
到现在 oauth2 三部分就这样配置完成 我们启动项目
先启动 eureka-server
再启动 oauth-server
进行如上配置之后,启动springboot应用就可以发现多了一些自动创建的endpoints:
Ant [pattern='/oauth/token'],
Ant [pattern='/oauth/token_key'],
Ant [pattern='/oauth/check_token']]]
重点关注一下/oauth/token,它是获取的token的endpoint。
启动springboot应用之后,使用http工具通过接口 /registry 进行注册
注册成功后 使用账号密码去登录
password模式:
http://localhost:8080/oauth/token? username=user&password=111111& grant_type=password&scope=select& client_id=client_2&client_secret=123456
看到成功返回token
在配置中,我们已经配置了对order资源的保护,如果直接访问: http://localhost:8080/order/1 ,会得到这样的响应
显示没有权限去访问
而对于未受保护的product资源 得到返回的结果
携带accessToken参数访问受保护的资源, 使用password模式获得的token
得到了之前匿名访问无法获取的资源
client模式
client模式:
http://localhost:8080/oauth/token?grant_type=client_credentials&scope=select&client_id=client_1&client_secret=123456
响应如下:
使用client模式获得的token 去访问
http://localhost:8080/order/1?access_token=xxxx
就这样简单的把oauth2 整合完成
后面我会进行网关统一认证以及其他更好的整合
里面涉及的东西很多 比较难理解 还是建议下载源码多看看
源码下载: https://github.com/LiHaodong888/springcloud-oauth2