基于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用于绕过认证服务访问被保护资源
密码模式流程

微服务之间的认证

网关与资源服务直接的访问将通过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<String> 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<String, String> body=new LinkedMultiValueMap<>();
//选择模式,密码模式
        body.add("grant_type","password");
        body.add("username",username);
        body.add("password",password);
        MultiValueMap<String, String> headers=new LinkedMultiValueMap<>();
      //进行http basic认证
        headers.add("Authorization",this.getBasic(clientId,clientSecret));
        HttpEntity<MultiValueMap<String,String>> 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<Map> 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);
    }
}

授权码模式

授权码模式流程

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351