基于Oauth2和spring security的用户权限认证

单点登陆

一处登录,处处登录,在微服务开发之中会存在很多的服务,当用户在某个服务之中登录成功,那么在这整个项目之中都登录成功
解决方案:Apache Shiro. CAS Spring security

Oauth2

Oauth2是一种开放资源授权的标准,是一种授权机制,主要用来颁发令牌(token)。主要常用的有两种授权模式:授权码模式和密码模式,此外还有隐藏式和客户端凭证两种方式
注意:不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。

密码模式

我在此详细讲讲密码模式的用户认证,授权码模式可参考其他。
1.当用户访问我们的资源服务时,若是已登录,cookie中将会存入短令牌jti,没有认证登录,cookie之中不存在短令牌
2.请求进入网关之后,如果没有短令牌jti存在并且不是登录请求的话,那么网关将直接返回请求至登录页面进行登录认证,用户使用账户密码的模式登录,登录请求将在网关中被直接放行进入认证服务进行认证,
3.认证服务会访问oauth2的封装接口/oauth/token来验证用户密码是否正确,如果验证通过秘钥证书签发token令牌相关信息同时进行授权,并且将其中的jti:token存入redis当中,jti为key,token为值,原因是token会很长,cookie可能放不下,cookie中将存入jti短令牌,用于其他服务来鉴权
4.当用户登录认证成功,cookie中已存入相应的jti短令牌时,再次访问其他被保护的资源,将会被网关进行增强,将redis中jwt令牌取出放入request请求头文件中放行,
5.在资源服务中放有公钥,配合security框架,资源服务将对携带在请求头文件的令牌进行解析,获取其中的用户信息,尤其是授权信息,在资源服务中的每个接口都会有相应的权限访问保护,由下面注解提供权限校验,如果具有相应权限,资源服务对请求放行,返回相应的数据给用户。

 @PreAuthorize("hasAnyAuthority('seckill_list')") //只有拥有seckill_list权限的人才可访问本接口
access_token:返回的令牌token
refresh_token:刷新令牌,用于令牌过期时使用,使用该令牌可以让令牌过期时间刷新
expires_in:过期时间
Jti:短令牌,将token存入redis,jti为key,并将jti存入cookie用于绕过认证服务访问被保护资源
基于Oauth2和spring security的用户权限认证_第1张图片
密码模式流程

微服务之间的认证

网关与资源服务直接的访问将通过request请求头中的jwt令牌信息完成鉴权认证,但是如果由网关进入一个资源服务,该服务需要去调用另外一个微服务来获取数据,那么微服务间的认证该如何进行?将如何把令牌从一个微服务传给另外一个微服务?
          由于微服务之间不像网关与微服务直接通过request的头文件传递令牌,从而让微服务拥有认证权限,微服务之间通过feign进行远程调用,没有传递request头文件,使得令牌无法传递,此时的解决方法是使用feign的拦截器,每次微服务调用之前都先检查下头文件,将请求的头文件中的令牌数据再放入到header中,再调用其他微服务即可。
微服务之间的认证很频繁,将拦截器放在common公共模块,拦截器将请求头文件中的jwt信息拦截并且存入header中,当某个服务需要feign远程调用其他服务时,在启动类注入启用该拦截器,即可进行令牌的传递完成认证
启动类启用拦截器:

    @Bean
    public FeignInterceptor feignInterceptor(){
        return  new FeignInterceptor();
    }

拦截器实现代码:

package com.changgou.interceptor;

import com.sun.org.apache.bcel.internal.generic.IF_ACMPEQ;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

@Component
public class FeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        //获得请求属性对象
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //获得request对象
        if (requestAttributes!=null){
            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
            if (request!=null){
                Enumeration headerNames = request.getHeaderNames();
                if (headerNames!=null){
                    while (headerNames.hasMoreElements()){
                        String headerName = headerNames.nextElement();
                        if ("authorization".equals(headerName)){
                            String headValue=request.getHeader(headerName);

                            requestTemplate.header(headerName,headValue);
                        }
                    }
                }
            }
        }
    }
}

认证服务主要代码

package com.changgou.oauth.service.impl;

import com.changgou.oauth.service.AuthService;
import com.changgou.oauth.util.AuthToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
public class AuthServiceImpl implements AuthService {
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private LoadBalancerClient loadBalancerClient;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${auth.ttl}")
    private long ttl;
    /**
     * 认证登录功能
     * @param username 用户名
     * @param password 密码
     * @param clientId 客户端Id
     * @param clientSecret 客户端秘钥
     * @return
     */
    @Override
    public AuthToken login(String username, String password, String clientId, String clientSecret) {
       //1.申请令牌
        ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
        URI uri = serviceInstance.getUri();
        String url=uri+"/oauth/token";

        MultiValueMap body=new LinkedMultiValueMap<>();
//选择模式,密码模式
        body.add("grant_type","password");
        body.add("username",username);
        body.add("password",password);
        MultiValueMap headers=new LinkedMultiValueMap<>();
      //进行http basic认证
        headers.add("Authorization",this.getBasic(clientId,clientSecret));
        HttpEntity> requestEntity=new HttpEntity<>(body,headers);
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
            @Override
            public void handleError(ClientHttpResponse response) throws IOException {
                if (response.getRawStatusCode()!=401&&response.getRawStatusCode()!=400) {
                    super.handleError(response);
                }
            }
        });
//核心代码,访问oauth2接口来获取token信息
        ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
        Map entityBody = responseEntity.getBody();
        if (CollectionUtils.isEmpty(entityBody)||entityBody.get("access_token")==null||entityBody.get("refresh_token")==null||entityBody.get("jti")==null){
                //申请令牌失败
            throw new RuntimeException("申请令牌失败");
        }
        //2.封装数据
        AuthToken authToken = new AuthToken();
        authToken.setAccessToken((String) entityBody.get("access_token"));
        authToken.setRefreshToken((String) entityBody.get("refresh_token"));
        authToken.setJti((String) entityBody.get("jti"));
        //3.将jti:token存入redis
        stringRedisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(),ttl,TimeUnit.SECONDS);
        return authToken;
    }

    private String getBasic(String clientId, String clientSecret) {
        String value=clientId+":"+clientSecret;
        byte[] encode = Base64Utils.encode(value.getBytes());
        return "Basic "+new String(encode);
    }
}

授权码模式

基于Oauth2和spring security的用户权限认证_第2张图片
授权码模式流程

获取用户信息之后,再根据信息关联查询用户表数据库,如果是第一次扫码登录,需要增加一条数据,根据我们的用户数据生成一个令牌给用户便于他下次登录
授权码模式详解:
https://www.jianshu.com/p/f3da08865ffb

你可能感兴趣的:(基于Oauth2和spring security的用户权限认证)