最近学习整理了下认证授权相关实现,下面是大概的一些理解与学习过程。在分布式系统中每个服务都需要认证,授权。如果每个服务都实现一套认证授权的逻辑就会显得冗余,考虑到分布式系统共享性的特点,我们可以独立一个授权服务出来,可以对内部系统或者第三方应用提供认证。
提供独立的认证服务,统一处理认证授权。不论是什么用户还是不同种类的客户端,例如小程序,APP,web,都采用一致的认证,权限,会话机制。同时保持开放性,可以接入第三方外部应用。
Oauth是一个开放标准协议,他允许用户授权第三方应用访问他们存储在服务提供者上的信息。不需要提供用户名密码等敏感信息,而只需要提供服务提供者允许且用户(客户端)需要的信息。
最常见的例子就是我们平时微信支付宝的第三方登录。用户在浏览器访问网站A.如果该网站接入了微信认证。那么他可以申请微信认证。微信发放授权码,令牌等一系列操作,然后网站A持有该令牌去获取微信用户的信息。
1、客户端请求第三方授权用户进入网站A的登录页面,点击微信的图标以微信账号登录系统,点击“微信”出现一个二维码,此时用户扫描二维码,开始给网站A授权。
2、用户同意给客户端授权资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证, 验证通过后,微信会询问用户是否给授权网站A访问自己的微信数据,用户点击“确认登录”表示同意授权,微信认证服务器会颁发一个授权码,并重定向到网站A.
3、客户端获取到授权码,请求认证服务器申请令牌此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码。
4、认证服务器向客户端响应令牌微信认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。此交互过程用户看不到,当客户端拿到令牌后,用户在网站A看到已经登录成功。
5、客户端请求资源服务器的资源客户端携带令牌访问资源服务器的资源。网站A携带令牌请求访问微信服务器获取用户的基本信息。
6、资源服务器返回受保护资源,资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。
以上流程只是oauth2.0的一种模式,即授权码模式。也是最oauth2中最安全的一种模式。授权码模式是oauth2中最安全的一种模式。观察上面的流程可以发现token始终没有经过浏览器也没有暴露密码,一切授权操作都是交给客户端的后台进行。因此这种模式对于第三方应用接入应用适用。
流程:
1.用户想访问客户端,直接提供用户名密码给客户端。
2.客户端将用户名,密码交给授权服务直接获取令牌。
密码模式相对于授权码模式简单一些,但没有授权码模式那么安全,因为他讲用户名密码暴露给了客户端。这种模式一般只适用于客户端是我们内部开发的情况,对于接入外部应用就不行了。
spring-Security-Oauth2是springcloud对oauth2的实现,oauth2的服务提供了两个服务。授权服务(Authorization Server)与资源服务(Resource server)
授权服务(UAA) (Authorization Server)应包含对接入端以及登入用户的合法性进行验证并颁发token等功能。
AuthorizationEndpoint 服务于认证请求。默认 URL: /oauth/authorize 。
TokenEndpoint 服务于访问令牌的请求。默认 URL: /oauth/token 。
在父工程下建立security-uaa授权服务。
创建授权服务配置类: 用@EnableAuthorizationServer注解并且实现AuthorizationServerConfigurerAdapte配置授权服务。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
}
AuthorizationServerConfigurerAdapter要求配置以下几个类:这几个对象由spring-oauth2进行管理,他们会被spring传入AuthorizationServerConfigurer中进行配置。
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
public AuthorizationServerConfigurerAdapter() {
}
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}
}
ClientDetailsServiceConfigurer:用来配置客户端详情服务,客户端的详细信息都在这里进行配置。客户端的信息在这里可以被写死,也可以将他们保存在数据库中。简单的来说这里就是来配置允许认证的客户端。
**AuthorizationServerEndpointsConfigurer:**用来配置令牌访问端点与令牌服务,申请令牌的url。令牌发放生成规则。
AuthorizationServerSecurityConfigurer:令牌的安全约束,访问令牌的安全约束。
ClientDetailsServiceConfigurer可以将客户端详细信息配置在数据库,也可以配置在内存中(clientDetailsService),
clientDetailsService:clientDetailsService负责查找clientDetails
clientDetails:clientDetails中包括以下几个属性。
clientId:(必须的)用来标识客户的Id。
secret:(需要值得信任的客户端)客户端安全码,如果有的话。
scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
authorities:此客户端可以使用的权限
使用内存方式存储userDetails:
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
//clients.withClientDetails(clientDetailsService); //存储在数据库中
clients.inMemory()// 存储在内存中
.withClient("c1")// client_id,客户端id,
.secret(new BCryptPasswordEncoder().encode("secret"))//客户端密钥
.resourceIds("res1","res2")//资源列表这里是一个列表可以配置多个
.authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
.scopes("all")// 允许的授权范围
.autoApprove(false)//false跳转到授权页面
//加上验证回调地址
.redirectUris("http://www.baidu.com"); //并且返回授权码
}
@Autowired
PasswordEncoder passwordEncoder;
//将客户端信息存储到数据库
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.withClientDetails(clientDetailsService);}
(1).配置令牌管理:
配置令牌存储单独写一个配置类进行配置,注入到spring容器中。
@Configuration
public class TokenConfig {
//采用对称加密的方式生成令牌,统一在网关层进行校验
private String SIGNING_KEY = "uaa123";
@Bean
public TokenStore tokenStore() {
//JWT令牌存储方案
return new JwtTokenStore(accessTokenConverter());
}
//配置令牌生成器
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
return converter;
}
}
(2).定义AuthorizationServerTokenServices
该接口实现了一些对令牌的管理。可以用它来修改令牌的格式与存储方式。
//引入配置的授权令牌存储策略
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
//令牌管理服务
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service=new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService);//客户端详情服务
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存储策略
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
配置授权码服务:AuthorizationServerEndpointsConfigurer 这个对象的实例可以完成令牌服务以及令牌endpoint配置。可以通过设置如下属性来决定支持的授权类型。
authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个 AuthenticationManager 对象。
authorizationCodeServices:这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对象),主要用于 “authorization_code” 授权码类型模式。
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
return new JdbcAuthorizationCodeServices(dataSource);//设置授权码模式的授权码如何存取
}
这里我们使用授权码模式,必须设置authorizationCodeServices。
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)//认证管理器
.authorizationCodeServices(authorizationCodeServices)//授权码服务
.tokenServices(tokenService())//令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security){
security
//oauth/token_key是公开
.tokenKeyAccess("permitAll()")
//oauth/check_token公开,开启token检查:资源服务拿到令牌可以请求oauth/check_token进行验证,如果采用jwt令牌的方式这里可以不用开启
.checkTokenAccess("permitAll()")
//表单认证(申请令牌)
.allowFormAuthenticationForClients();
}
1.tokenkey这个endpoint当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个
endpoint完全公开。
2.checkToken这个endpoint完全公开
3.允许表单认证
创建资源服务配置类
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResouceServerConfig extends
ResourceServerConfigurerAdapter {
//资源的标识,这里与授权服务中clientDetailsService中配置的resourceIds("res1","res2")相对应
public static final String RESOURCE_ID = "res1";
//这里配置资源服务
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)
.tokenServices(tokenService())
.stateless(true);
}
//这里的配置类似于HttpSecurity访问权限等一系列配置
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
//这里配置授权服务中允许访问的范围。
.antMatchers("/**").access("#oauth2.hasScope('all')")
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
流程:
接入方经过网关请求授权服务授权,授权服务验证,并返回token给接入应用
接入方携带token访问资源。
在网关层,会校验该接入方的访问权限,并且解析token为明文,将该token下发给对应资源服务
在资源服务拿到token后,还会继续根据token中的身份信息来校验是否有权限访问对应资源。
所有微服务的请求都经过网关,网关从注册中心读取微服务的地址,将请求转发至微服务。
配置application.yml
spring:
application:
name: security_discovery
server:
port: 53000 #启动端口
eureka:
server:
enable-self-preservation: false #关闭服务器自我保护,客户端心跳检测15分钟内错误达到80%服务会保护,导致别人还认为是好用的服务
client:
register-with-eureka: false #false:不作为一个客户端注册到注册中心
instance-info-replication-interval-seconds: 10
serviceUrl:
defaultZone: http://localhost:${server.port}/eureka/
认证服务器生成jwt令牌,所有请求都会通过网关层校验。在网关层拦截请求获取令牌解析,并转发给对应的服务。这样各资源服务就不需要关注令牌解析了。网关就从当了上文中资源服务器的角色。
网关采用zuul,统一认证服务与资源服务都是网关下微服务,需要在网关上新增路由配置
zuul.routes.uaa‐service.stripPrefix = false
zuul.routes.uaa‐service.path = /uaa/**
zuul.routes.user‐service.stripPrefix = false
zuul.routes.user‐service.path = /source/**
1.从请求中获取令牌内容。
2.组装明文token,转发给微服务,放入请求头中
public class AuthFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
//从Security上下文中拿到用户身份对象
SecurityContext authentication = SecurityContextHolder.getContext();
//判断其是否是oauth授予的身份信息
if(!(authentication instanceof OAuth2Authentication)){
return null;
}
OAuth2Authentication oAuth2Authentication =(OAuth2Authentication) authentication;
Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
// 取出用户身份信息
String principal = userAuthentication.getName();
// 取出用户权限
List<String> authorities = new ArrayList<>();
Collection<? extends GrantedAuthority> authorities1 = userAuthentication.getAuthorities();
for(GrantedAuthority authentication1:authorities1 ) {
authorities.add(authentication1.getAuthority());
}
OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
Map<String,Object> jsonToken = new HashMap<>(requestParameters);
if(userAuthentication != null) {
jsonToken.put("principal" ,principal);
jsonToken.put("authorities",authorities);
}
// 把用户身份信息和授权信息放在json中,加入http的header中,转发给微服务
//对其进行base64编码
currentContext.addZuulRequestHeader("json-token",EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));
return null;
}
}
配置令牌配置类:
在授权服务中我配置了采用对称加密方式生成令牌的配置类,这里直接拷贝到网关服务。
public class TokenConfig {
private String SIGNING_KEY = "uaa123";
@Bean
public TokenStore tokenStore() {
//JWT令牌存储方案
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
return converter;
}
主要配置的内容就是定义一些匹配规则,描述某个接入客户端需要什么样的权限才能访问某个微服务
@Configuration
public class ResouceServerConfig {
//资源的标识,这里与授权服务中clientDetailsService中配置的resourceIds("res1","res2")相对应
public static final String RESOURCE_ID = "res1";
//这里是对授权服务请求进行处理
@Configuration
@EnableResourceServer
public class UAAServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
//这里配置只要是授权服务的请求都不进行拦截
http.authorizeRequests()
.antMatchers("/uaa/**").permitAll();
}
}
//这里是对3.2中的资源服务的请求进行处理
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
//这里对令牌进行解析
resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
//这里配置授权服务中允许访问的范围。
.antMatchers("/resource/**").access("#oauth2.hasScope('ROLE_API')");
}
}
//配置其它的资源服务..
}
在下游微服务拿到token后,应该怎么实现权限拦截?自己解析明文token,自己实现一套访问策略?在这里还是可以使用spring security来认证token,在资源服务中新建filter,用于从请求中获取token并解析,拿到用户身份信息与权限,将其放入springsecurity上下文中。
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//解析出头中的token
String token = httpServletRequest.getHeader("json-token");
if(token!=null){
String json = EncryptUtil.decodeUTF8StringBase64(token);
//将token转成json对象
JSONObject jsonObject = JSON.parseObject(json);
//用户身份信息
UserDTO userDTO = JSON.parseObject(jsonObject.getString("principal"), UserDTO.class);
//用户权限
JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);
//将用户信息和权限填充 到用户身份token对象中
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
//将authenticationToken填充到安全上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}