精华内容
下载资源
问答
  • spring boot 使用 github进行第三方登录

    千次阅读 2019-06-12 00:07:52
    github作为网站的第三方登录,首先需要了解github给我们提供了怎样的api.登录github官网(略),点击底部“API” 下面这个就是我们需要用的,点击“Read the docs”看看具体怎么说 可以看到github给我们提供了...

    源码:https://github.com/lvsdian/community

    用github作为网站的第三方登录,首先需要了解github给我们提供了怎样的api.登录github官网(略),点击底部“API”

     下面这个就是我们需要用的,点击“Read the docs”看看具体怎么说

     可以看到github给我们提供了相当多的api,此次我们需要用到就是这个:

    总共四个步骤。

    首先,创建一个OAuth App。 点击蓝色的的字体,进去可以看到详细说明和图片示例,不得不说对我们英语渣github做的还是很友好的。这是github提供的教程:

    这里的OAuth App可以理解为一个用作认证的应用,你既然用github作第三方登录,总得给他个说法对吧,这个OAuth app 也可以对你的项目在认证这部分有一定的管理作用。

    接着,我们看看具体应该怎么操作。 

    我们回到登录后的界面,点击右上头像旁边的下三角==》settings==》左侧最下选择 Developer settings==》左侧OAuth Apps,右侧点击“New OAuth App”即可按到达教程上创建 OAuth App的页面。

    创建完OAuth App后进行第二步。

    官方教程如下,第二步即上传一个logo

     这个logo及刚刚创建的OAuth App名称后面在用户登录时展示出来,如下(这里提前把图贴出来了):

    刚刚第一步点击 “Register application”后进入到如下页面

    上传 logo后 Update application 即可完成第二步。

    至此,前两步完成,较为简单,填一下基本信息即可。

    第三步较为繁琐,分为三小步,官网教程:

    第一步,用户登录时重定向到github

    https://github.com/login/oauth/authorize

    指定的六个参数(client_id、redirect_uri、login、scope、state、allow_signup)中,redirect_url在第一步创建OAuth App时已指定,即callbackURL,而client_id在OAuth App创建成功后会自动生成。

    用github验证用户的登录,若成功即获取gitub的授权。

    第二步,用户成功登陆后,github跳转回来的地址会携带一个“code”,并且会把上一步6个参数中的state返回来,我们要写代码接收这个code,并且写一个post请求,发送到以下access_token接口。

    https://github.com/login/oauth/access_token

     这个接口会返回一个accessToken字符串。

     第三步,通过user这个API,第二步获得的token为参数就可以拿到具体的用户信息。

    https://api.github.com/user

    整个过程的时序图:

     下面在代码中具体实现这个过程。

    第一步,用户登录时需要重定向,所以我们应该给页面中的登录按钮绑定一个地址:

    get方式请求,地址是

    https://github.com/login/oauth/authorize

     client_id复制粘贴即可;

    redicect_url前面已指定;

    login参数官方是建议用一个account去授权这个app,不清楚怎么写,不填;

    scope就是你想要得到什么,比如你想要邮箱信息,就写 scope=user:email,我这里就写个 scope=user;

    state是一个random String,用于防止跨站点请求伪造攻击,我们就随便写个 1(这里只是演示,正式项目开发可能用程序实现好点);

    allow_signup即是否让没有经过身份验证的用户注册GitHub。默认值为true。如果禁止注册,使用false,我们可以不用管。

    <a href="https://github.com/login/oauth/authorize?client_id=2533b2dbb47cfaa0a273&redirect_uri=http://localhost:8080/callback&scope=user&state=1">登录</a>
    

    将登陆按钮设置如上标签后,用户点击登陆即会跳转:
     

    第一步完成。

    第二步,用户如果正确登陆github,会跳转到第一步配的redirect_url 

    localhost:8080/callback

    并且带上state和code参数。所以我们可以在controller中写一个控制器接收callback这个请求。带上这两个参数向

    https://github.com/login/oauth/access_token

     发送一个post请求,用java发送post我们引入 okhttp 这个工具,在pom.xml中引入依赖即可(这里有个坑:我引入okhttp版本为:4.0.0-RC1,spring boot 版本为 2.1.5.RELEASE,jdk8时,spring boot的那个main方法不能启动,即项目不能运行,但也不报错,只能通过maven启动项目,更改如下okhttp版本后恢复正常 )。

            <!-- 引入okHttp -->
            <dependency>
                <groupId>com.squareup.okhttp3</groupId>
                <artifactId>okhttp</artifactId>
                <version>3.14.1</version>
            </dependency>

    根据官方教程,发送这个post请求应携带5个参数,client_id、client_secret、code、redirect_url、state,其中client_id与client_secret在创建的OAuth app中可以找到,code与state为前面callback请求的参数,redirect_url还是前面的那个,

    application.properties中配置好这三个参数的值

    github.client.id=2533b2dbb47cfaa0a273
    github.client.secret=195ef396a84cdf9bb18462fd91d5f835d25895d4
    github.redirect.uri=http://localhost:8080/callback

    我们将这5个参数封装为一个对象AccessTokenDTO:

    public class AccessTokenDTO {
        private String client_id;
        private String client_secret;
        private String code;
        private String redirect_uri;
        private String state;
        
        //getter与setter省了
        ...
    }

     发送这个post请求后,会返回一个accessToken字符串

    第三步,带上这个accessToken字符串向

    https://api.github.com/user

    发送一个get请求,返回值就是我们想要的用户信息。 

    最终我们获得的用户信息也可以封装成一个对象(具体来说,我们可以获得用户的哪些信息,在得到accessToken后可在浏览器通过https://api.github.com/user?access_token=XXX得到一个json,这个json就是用户信息,这里只取3个):

    public class GithubUser {
        private String name;
        private Long id;
        private String bio;
        
        //getter、setter省了
        ...
    }

     将okhttp发送向

    https://github.com/login/oauth/access_token
    https://api.github.com/user

     发送请求也封装起来,如下:

    @Component
    public class GithubProvider {
        /**
         * 使用okhttp根据传过来的accessTokenDTO对象,向https://github.com/login/oauth/access_token发送post请求获得token
         *
         * @param accessTokenDTO
         * @return
         */
        public String getAccessToken(AccessTokenDTO accessTokenDTO) {
            MediaType mediaType = MediaType.get("application/json; charset=utf-8");
            OkHttpClient client = new OkHttpClient();
    
            RequestBody body = RequestBody.create(mediaType, JSON.toJSONString(accessTokenDTO));
            Request request = new Request.Builder()
                    .url("https://github.com/login/oauth/access_token")
                    .post(body)
                    .build();
            try (
                    Response response = client.newCall(request).execute()) {
                    String string = response.body().string();
                    System.out.println(string);//access_token=c231d1e359bed82f6f98b2a2d86eb916478b56d1&scope=user&token_type=bearer
                    String token = string.split("&")[0].split("=")[1];
                    return token;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
    
        /**
         * 使用okhttp根据传过来的accessToken向https://api.github.com/user发送请求获得user
         *
         * @param accessToken
         * @return
         */
        public GithubUser getUser(String accessToken) {
            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    .url("https://api.github.com/user?access_token=" + accessToken)
                    .build();
            try (
                    Response response = client.newCall(request).execute()) {
                    String string = response.body().string();
                    return JSON.parseObject(string, GithubUser.class);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    

    我们在controller中通过GithubProvider封装的两个方法直接获取用户信息(user)。

    @Controller
    public class AuthorizeController {
    
        @Autowired
        private GithubProvider githubProvider;
    
        @Value("${github.client.id}")
        private String clientId;
    
        @Value("${github.client.secret}")
        private String clientSecret;
    
        @Value("${github.redirect.uri}")
        private String redirectUri;
    
        @GetMapping("/callback")
        public String callback(@RequestParam(name = "code")String code,
                               @RequestParam(name = "state")String state,
                               HttpServletRequest request){
    
            AccessTokenDTO accessTokenDTO = new AccessTokenDTO();
            accessTokenDTO.setRedirect_uri(redirectUri);
            accessTokenDTO.setClient_id(clientId);
            accessTokenDTO.setCode(code);
            accessTokenDTO.setState(state);
            accessTokenDTO.setClient_secret(clientSecret);
    
            String accessToken = githubProvider.getAccessToken(accessTokenDTO);
            GithubUser user = githubProvider.getUser(accessToken);
    
            System.out.println("user.getId():" + user.getId());
    
    
            if(user != null){
                //登录成功 写入cookie和session
                request.getSession().setAttribute("user",user);
                return "redirect:/";
            }else{
                //登录失败  重新登录
                return "redirect:/";
            }
        }
    
        @RequestMapping("/")
        public String index(){
            return "index";
        }
    }

    将user放在session中,首页使用即可

                <ul class="nav navbar-nav navbar-right">
                    <li class="dropdown" th:if="${session.user != null}">
                        <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false" th:text="${session.user.getId()}"> <span class="caret"></span></a>
                        <ul class="dropdown-menu">
                            <li><a href="#">消息中心</a></li>
                            <li><a href="#">个人资料</a></li>
                            <li><a href="#">退出登录</a></li>
                        </ul>
                    </li>
                    <li th:if="${session.user == null}">
                        <a href="https://github.com/login/oauth/authorize?client_id=2533b2dbb47cfaa0a273&redirect_uri=http://localhost:8080/callback&scope=user&state=1">登录</a>
                    </li>
                </ul>

    完结 撒花!!!

    展开全文
  • elastic-email-client:该项目是Spring Boot Email Client的延续,也是来自该GitHub帐户的。 本质上,该项目使用Lucene和ElasticSearch堆栈从服务器端获取不同的资源。
  • 点击上方Java后端,选择设为星标优质文章,及时送达链接:zyc.red/Spring/Security/OAuth2/OAuth2-Client/前言OAuth(开放授权)是一个开...

    点击上方 Java后端,选择 设为星标

    优质文章,及时送达


    链接:zyc.red/Spring/Security/OAuth2/OAuth2-Client/

    前言

    OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。网上有很多关于OAuth协议的讲解,这里就不在详细解释OAuth相关的概念了,请读者自行查阅相关资料,否则本文接下来的内容可能会很难理解。

    Spring-Security对OAuth2.0的支持

    截止到本文撰写的日期为止,Spring已经提供了对OAuth提供的支持(spring-security-oauth:https://github.com/spring-projects/spring-security-oauth),但是该工程已经被废弃了,因为Spring-Security工程提供了最新的OAuth2.0支持。如果你的项目中使用了过期的Spring-Security-OAuth,请参考《OAuth 2.0迁移指南:https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide》,

    本文将对OAuth2.0中的客户端模式进行原理分析,结合Spring官方指南中提供了一个简单的基于spring-boot与oauth2.0集成第三方应用登录的案例(spring-boot-oauth2:https://spring.io/guides/tutorials/spring-boot-oauth2/),一步一步分析其内部实现的原理。

    公众号同样发布过近百篇 Spring Boot 相关的实战文章,关注微信公众号 Java后端,回复 666 下载这本技术栈手册。

    创建GitHub OAuth Apps

    在Github OAuth Apps中创建一个新的应用

    这个应用相当于我们自己的应用(客户端),被注册在Github(授权服务器)中了,如果我们应用中的用户有github账号的话,则可以基于oauth2来登录我们的系统,替代原始的用户名密码方式。在官方指南的例子中,使用spring-security和oauth2进行社交登陆只需要在你的pom文件中加入以下几个依赖即可:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    然后在配置文件中填上刚刚注册的应用的clientId和clientSecret:

    spring:
      security:
        oauth2:
          client:
            registration:
              github:
                clientId: github-client-id
                clientSecret: github-client-secret

    紧接着就像普通的spring-security应用一样,继承WebSecurityConfigurerAdapter,进行一些简单的配置即可:

    @SpringBootApplication
    @RestController
    public class SocialApplication extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
          
            http
                .authorizeRequests(a -> a
                    .antMatchers("/", "/error", "/webjars/**").permitAll()
                    .anyRequest().authenticated()
                )
                .exceptionHandling(e -> e
                    .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
                )
                .oauth2Login();
            
        }
    }

    也就是说我们只需要添加maven依赖以及继承WebSecurityConfigurerAdapter进行一些简单的配置,一个oauth2客户端应用就构建完成了。接下来按照指南上的步骤点击页面的github登录链接我们的页面就会跳转到github授权登录页,等待用户授权完成之后浏览器重定向到我们的callback URL最终请求user信息端点即可访问到刚刚登入的github用户信息,整个应用的构建是如此的简单,背后的原理是什么呢?接下来我们开始分析。

    还是和以前一样,我们在配置文件中将security的日志级别设置为debug

    logging:
      level:
        org.springframework.security: debug

    重新启动应用之后,从控制台输出中我们可以看到与普通spring-security应用不同的地方在于整个过滤链多出了以下几个过滤器:

    OAuth2AuthorizationRequestRedirectFilter
    OAuth2LoginAuthenticationFilter

    联想oauth2的授权码模式以及这两个过滤器的名字,熟悉spring-security的同学心中肯定已经有了一点想法了。对没错,spring-security对客户端模式的支持完全就是基于这两个过滤器来实现的。现在我们来回想以下授权码模式的执行流程

    1. 用户在客户端页面点击三方应用登录按钮(客户端就是我们刚刚注册的github应用)

    2. 页面跳转到三方应用注册的授权方页面(授权服务器即github)

    3. 用户登入授权后,github调用我们应用的回调地址(我们刚刚注册github应用时填写的回调地址)

    4. 第三步的回调地址中github会将code参数放到url中,接下来我们的客户端就会在内部拿这个code再次去调用github

      的access_token地址获取令牌

    上面就是标准的authorization_code授权模式,OAuth2AuthorizationRequestRedirectFilter的作用就是上面步骤中的1.2步的合体,当用户点击页面的github授权url之后,OAuth2AuthorizationRequestRedirectFilter匹配这个请求,接着它会将我们配置文件中的clientId、scope以及构造一个state参数(防止csrf攻击)拼接成一个url重定向到github的授权url,OAuth2LoginAuthenticationFilter的作用则是上面3.4步骤的合体,当用户在github的授权页面授权之后github调用回调地址,OAuth2LoginAuthenticationFilter匹配这个回调地址,解析回调地址后的code与state参数进行验证之后内部拿着这个code远程调用github的access_token地址,拿到access_token之后通过OAuth2UserService获取相应的用户信息(内部是拿access_token远程调用github的用户信息端点)最后将用户信息构造成Authentication被SecurityContextPersistenceFilter过滤器保存到HttpSession中。

    下面我们就来看一下这两个过滤器内部执行的原理:

    public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
        
        ......省略部分代码
    
      @Override
      protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
          throws ServletException, IOException {
    
        try {
          OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
          if (authorizationRequest != null) {
            this.sendRedirectForAuthorization(request, response, authorizationRequest);
            return;
          }
        } catch (Exception failed) {
          this.unsuccessfulRedirectForAuthorization(request, response, failed);
          return;
        }
            ......省略部分代码
    }

    通过authorizationRequestResolver解析器解析请求,解析器的默认实现是DefaultOAuth2AuthorizationRequestResolver,核心解析方法如下:

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        
        String registrationId = this.resolveRegistrationId(request);
        String redirectUriAction = getAction(request, "login");
        return resolve(request, registrationId, redirectUriAction);
    }
    
    
    private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) {
        if (registrationId == null) {
            return null;
        }
      
        ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
        if (clientRegistration == null) {
            throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
        }
    
        Map<String, Object> attributes = new HashMap<>();
        attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
    
        OAuth2AuthorizationRequest.Builder builder;
        
        if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
            builder = OAuth2AuthorizationRequest.authorizationCode();
            Map<String, Object> additionalParameters = new HashMap<>();
            if (!CollectionUtils.isEmpty(clientRegistration.getScopes()) &&
                clientRegistration.getScopes().contains(OidcScopes.OPENID)) {
                
                
                
                addNonceParameters(attributes, additionalParameters);
            }
            if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
                addPkceParameters(attributes, additionalParameters);
            }
            builder.additionalParameters(additionalParameters);
        } else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
            builder = OAuth2AuthorizationRequest.implicit();
        } else {
            throw new IllegalArgumentException("Invalid Authorization Grant Type ("  +
                                               clientRegistration.getAuthorizationGrantType().getValue() +
                                               ") for Client Registration with Id: " + clientRegistration.getRegistrationId());
        }
    
        String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
    
        OAuth2AuthorizationRequest authorizationRequest = builder
            .clientId(clientRegistration.getClientId())
            .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
            .redirectUri(redirectUriStr)
            .scopes(clientRegistration.getScopes())
            
            .state(this.stateGenerator.generateKey())
            .attributes(attributes)
            .build();
    
        return authorizationRequest;
    }

    DefaultOAuth2AuthorizationRequestResolver判断请求是否是授权请求,最终返回一个OAuth2AuthorizationRequest对象给OAuth2AuthorizationRequestRedirectFilter,如果OAuth2AuthorizationRequest不为null的话,说明当前请求是一个授权请求,那么接下来就要拿着这个请求重定向到授权服务器的授权端点了,下面我们接着看OAuth2AuthorizationRequestRedirectFilter发送重定向的逻辑:

    private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
                                              OAuth2AuthorizationRequest authorizationRequest) throws IOException {
    
        if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {
            this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
        }
        this.authorizationRedirectStrategy.sendRedirect(request, response, authorizationRequest.getAuthorizationRequestUri());
    }

    1. 如果当前是授权码类型的授权请求那么就需要将这个请求信息保存下来,因为接下来授权服务器回调我们需要用到这个授权请求的参数进行校验等操作(比对state),这里是通过authorizationRequestRepository保存授权请求的,默认的保存方式是通过HttpSessionOAuth2AuthorizationRequestRepository保存在httpsession中的,具体的保存逻辑很简单,这里就不细说了。

    2. 保存完成之后就要开始重定向到授权服务端点了,这里默认的authorizationRedirectStrategy是DefaultRedirectStrategy,重定向的逻辑很简单,通过response.sendRedirect方法使前端页面重定向到指定的授权

    public void sendRedirect(HttpServletRequest request, HttpServletResponse response,
                             String url) throws IOException {
        String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
        redirectUrl = response.encodeRedirectURL(redirectUrl);
    
        if (logger.isDebugEnabled()) {
            logger.debug("Redirecting to '" + redirectUrl + "'");
        }
    
        response.sendRedirect(redirectUrl);
    }

    OAuth2AuthorizationRequestRedirectFilter处理逻辑讲完了,下面我们对它处理过程做一个总结

    a. 通过内部的OAuth2AuthorizationRequestResolver解析当前的请求,返回一个OAuth2AuthorizationRequest对象,如果当前请求是授权端点请求,那么就会返回一个构造好的对象,包含我们的client_id、state、redirect_uri参数,如果对象为null的话,那么就说明当前请求不是授权端点请求。

    注意如果OAuth2AuthorizationRequestResolver不为null的话,OAuth2AuthorizationRequestResolver内部会将其保存在httpsession中这样授权服务器在调用我们的回调地址时我们就能从httpsession中取出请求将state进行对比以防csrf攻击。

    b. 如果第一步返回的OAuth2AuthorizationRequest对象不为null的话,接下来就会通过response.sendRedirect的方法将OAuth2AuthorizationRequest中的授权端点请求发送到前端的响应头中然后浏览器就会重定向到授权页面,等待用户授权。

    OAuth2LoginAuthenticationFilter

    public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
        @Override
      public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
          throws AuthenticationException {
    
        MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
            
        if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
          OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
          throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }
        
            
        OAuth2AuthorizationRequest authorizationRequest =
            this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
        if (authorizationRequest == null) {
          OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
          throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }
        
            
        String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);
        ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
        if (clientRegistration == null) {
          OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,
              "Client Registration not found with Id: " + registrationId, null);
          throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }
        String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
            .replaceQuery(null)
            .build()
            .toUriString();
        OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
    
        Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
        OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(
            clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
        authenticationRequest.setDetails(authenticationDetails);
        
        OAuth2LoginAuthenticationToken authenticationResult =
          (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);
        
        OAuth2AuthenticationToken oauth2Authentication = new OAuth2AuthenticationToken(
          authenticationResult.getPrincipal(),
          authenticationResult.getAuthorities(),
          authenticationResult.getClientRegistration().getRegistrationId());
        oauth2Authentication.setDetails(authenticationDetails);  
            
        OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
          authenticationResult.getClientRegistration(),
          oauth2Authentication.getName(),
          authenticationResult.getAccessToken(),
          authenticationResult.getRefreshToken());
    
        this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
    
        return oauth2Authentication;
      }
    }

    OAuth2LoginAuthenticationFilter的作用很简单,就是响应授权服务器的回调地址,核心之处在于OAuth2LoginAuthenticationProvider对OAuth2LoginAuthenticationToken的认证,

    OAuth2LoginAuthenticationToken

    OAuth2LoginAuthenticationProvider

    public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
        
         ...省略部分代码
        
        @Override
      public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        OAuth2LoginAuthenticationToken authorizationCodeAuthentication =
          (OAuth2LoginAuthenticationToken) authentication;
    
        
        
        
        if (authorizationCodeAuthentication.getAuthorizationExchange()
          .getAuthorizationRequest().getScopes().contains("openid")) {
          
          
          return null;
        }
    
        OAuth2AccessTokenResponse accessTokenResponse;
        try {
          OAuth2AuthorizationExchangeValidator.validate(
              authorizationCodeAuthentication.getAuthorizationExchange());
          
          accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
              new OAuth2AuthorizationCodeGrantRequest(
                  authorizationCodeAuthentication.getClientRegistration(),
                  authorizationCodeAuthentication.getAuthorizationExchange()));
    
        } catch (OAuth2AuthorizationException ex) {
          OAuth2Error oauth2Error = ex.getError();
          throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }
        
             
        OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();
        Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
        
             
        OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
            authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters));
    
        Collection<? extends GrantedAuthority> mappedAuthorities =
          this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
        
             
        OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
          authorizationCodeAuthentication.getClientRegistration(),
          authorizationCodeAuthentication.getAuthorizationExchange(),
          oauth2User,
          mappedAuthorities,
          accessToken,
          accessTokenResponse.getRefreshToken());
        authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
    
        return authenticationResult;
      }
        ...省略部分代码
    }

    OAuth2LoginAuthenticationProvider的执行逻辑很简单,首先通过code获取access_token,然后通过access_token获取用户信息,这和标准的oauth2授权码模式一致。

    自动配置

    在spring指南的例子中,我们发现只是配置了一个简单oauth2Login()方法,一个完整的oauth2授权流程就构建好了,其实这完全归功于spring-boot的autoconfigure,我们找到spring-boot-autoconfigure.jar包中的security.oauth2.client.servlet包,可以发现spring-boot给我们提供了几个自动配置类:

    OAuth2ClientAutoConfiguration
    OAuth2ClientRegistrationRepositoryConfiguration
    OAuth2WebSecurityConfiguration

    其中OAuth2ClientAutoConfiguration导入了OAuth2ClientRegistrationRepositoryConfiguration和OAuth2WebSecurityConfiguration的配置

    OAuth2ClientRegistrationRepositoryConfiguration:

    @Configuration(proxyBeanMethods = false)
    @EnableConfigurationProperties(OAuth2ClientProperties.class)
    @Conditional(ClientsConfiguredCondition.class)
    class OAuth2ClientRegistrationRepositoryConfiguration {
    
        @Bean
        @ConditionalOnMissingBean(ClientRegistrationRepository.class)
        InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) {
            List<ClientRegistration> registrations = new ArrayList<>(
                OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
            return new InMemoryClientRegistrationRepository(registrations);
        }
    
    }

    OAuth2ClientRegistrationRepositoryConfiguration将我们在配置文件中注册的client构造成ClientRegistration然后保存到内存之中。这里有一个隐藏的CommonOAuth2Provider类,这是一个枚举类,里面事先定义好了几种常用的三方登录授权服务器的各种参数例如GOOGLE、GITHUB、FACEBOO、OKTA

    CommonOAuth2Provider

    public enum CommonOAuth2Provider {
    
      GOOGLE {
    
        @Override
        public Builder getBuilder(String registrationId) {
          ClientRegistration.Builder builder = getBuilder(registrationId,
              ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
          builder.scope("openid", "profile", "email");
          builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
          builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
          builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");
          builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
          builder.userNameAttributeName(IdTokenClaimNames.SUB);
          builder.clientName("Google");
          return builder;
        }
      },
    
      GITHUB {
    
        @Override
        public Builder getBuilder(String registrationId) {
          ClientRegistration.Builder builder = getBuilder(registrationId,
              ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
          builder.scope("read:user");
          builder.authorizationUri("https://github.com/login/oauth/authorize");
          builder.tokenUri("https://github.com/login/oauth/access_token");
          builder.userInfoUri("https://api.github.com/user");
          builder.userNameAttributeName("id");
          builder.clientName("GitHub");
          return builder;
        }
      },
    
      FACEBOOK {
    
        @Override
        public Builder getBuilder(String registrationId) {
          ClientRegistration.Builder builder = getBuilder(registrationId,
              ClientAuthenticationMethod.POST, DEFAULT_REDIRECT_URL);
          builder.scope("public_profile", "email");
          builder.authorizationUri("https://www.facebook.com/v2.8/dialog/oauth");
          builder.tokenUri("https://graph.facebook.com/v2.8/oauth/access_token");
          builder.userInfoUri("https://graph.facebook.com/me?fields=id,name,email");
          builder.userNameAttributeName("id");
          builder.clientName("Facebook");
          return builder;
        }
      },
    
      OKTA {
    
        @Override
        public Builder getBuilder(String registrationId) {
          ClientRegistration.Builder builder = getBuilder(registrationId,
              ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
          builder.scope("openid", "profile", "email");
          builder.userNameAttributeName(IdTokenClaimNames.SUB);
          builder.clientName("Okta");
          return builder;
        }
      };
    
      private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}";
    
      protected final ClientRegistration.Builder getBuilder(String registrationId,
                                  ClientAuthenticationMethod method, String redirectUri) {
        ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(registrationId);
        builder.clientAuthenticationMethod(method);
        builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
        builder.redirectUriTemplate(redirectUri);
        return builder;
      }
        
      public abstract ClientRegistration.Builder getBuilder(String registrationId);
    
    }

    这就是为什么我们没有配置github授权端点确能够跳转授权页面的原因。

    OAuth2WebSecurityConfiguration

    OAuth2WebSecurityConfiguration配置一些web相关的类,像如何去保存和获取已经授权过的客户端,以及默认的oauth2客户端相关的配置

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnBean(ClientRegistrationRepository.class)
    class OAuth2WebSecurityConfiguration {
    
      @Bean
      @ConditionalOnMissingBean
      OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {
        return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
      }
    
      @Bean
      @ConditionalOnMissingBean
      OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) {
        return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
      }
      
        
      @Configuration(proxyBeanMethods = false)
      @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
      static class OAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
          http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
          http.oauth2Login(Customizer.withDefaults());
          http.oauth2Client();
        }
    
      }
    
    }

    参考:

    集成GitHub和QQ社交登录

    https://github.com/Allurx/spring-security-oauth2-demo/tree/master/spring-security-oauth2-client

    spring-security-oauth更新路线

    https://spring.io/blog/2019/11/14/spring-security-oauth-2-0-roadmap-update

    spring-security对oauth2.0授权服务器的支持

    https://github.com/spring-projects/spring-security/issues/6320

    使用spring-boot和oauth2.0构建社交登陆

    https://spring.io/guides/tutorials/spring-boot-oauth2/

    - END -

    最近整理一份面试资料《Java技术栈学习手册》,覆盖了Java技术、面试题精选、Spring全家桶、Nginx、SSM、微服务、数据库、数据结构、架构等等。获取方式:点“ 在看,关注公众号 Java后端 并回复 777 领取,更多内容陆续奉上。推荐阅读 1. 15000 字 30 图讲解 AQS2. 如何设计订单系统?不妨看看这篇文章3. 用命令行管理你的 GitHub 项目,不必再开网页4. 怎么去掉 IDEA 中 XML 显示的屎黄色5. 程序员需知的 58 个网站喜欢文章,点个在看 
    
    展开全文
  • 点击上方好好学java,选择星标公众号 重磅资讯,干货,第一时间送达 今日推荐:14 个 github 项目!个人原创100W +访问量博客:点击前往,查看更多 本文地址:htt...
    点击上方 好好学java ,选择 星标 公众号
    
    重磅资讯,干货,第一时间送达
    今日推荐:14 个 github 项目!个人原创100W +访问量博客:点击前往,查看更多
    

    本文地址:https://www.zyc.red/Spring/Security/OAuth2/OAuth2-Client/

    松哥在四月份出过一个 OAuth2 教程(公号后台回复 OAuth2 可以获取),里边也和大家分享了 GitHub 第三方登录,但是我用的是一个更加通用的方式,就是自己配置各种回调,自己去请求各种数据,效果虽然实现了,但是比较麻烦。最近松哥在网上看到一篇文章,里边 Spring Security 自带的 OAuth2 登录功能,再结合 Spring Boot 的自动化配置,这种实现方式只需要在 application.properties 中配置一下 clientid 和 clientsecret 即可,今天就和大家分享一下这种方案,以下是正文(文章细节可能不是很详细,但是思路没问题)。

    概述

    OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。网上有很多关于OAuth协议的讲解,这里就不在详细解释OAuth相关的概念了,不了解的小伙伴可以在公号后台回复 OAuth2 获取教程链接。

    Spring-Security 对 OAuth2.0的支持

    截止到本文撰写的日期为止,Spring已经提供了对OAuth提供的支持(spring-security-oauth),但是该工程已经被废弃了,因为Spring-Security工程提供了最新的OAuth2.0支持。如果你的项目中使用了过期的Spring-Security-OAuth,请参考《OAuth 2.0迁移指南》,本文将对OAuth2.0中的客户端模式进行原理分析,结合Spring官方指南中提供了一个简单的基于spring-boot与oauth2.0集成第三方应用登录的案例(spring-boot-oauth2),一步一步分析其内部实现的原理。

    创建GitHub OAuth Apps

    在Github OAuth Apps中创建一个新的应用

    这个应用相当于我们自己的应用(客户端),被注册在Github(授权服务器)中了,如果我们应用中的用户有github账号的话,则可以基于oauth2来登录我们的系统,替代原始的用户名密码方式。在官方指南的例子中,使用spring-security和oauth2进行社交登陆只需要在你的pom文件中加入以下几个依赖即可

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    

    然后在配置文件中填上刚刚注册的应用的clientId和clientSecret

    spring:
      security:
        oauth2:
          client:
            registration:
              github:
                clientId: github-client-id
                clientSecret: github-client-secret
    

    紧接着就像普通的spring-security应用一样,继承WebSecurityConfigurerAdapter,进行一些简单的配置即可

    @SpringBootApplication
    @RestController
    public class SocialApplication extends WebSecurityConfigurerAdapter {
    
        // ...
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
         // @formatter:off
            http
                .authorizeRequests(a -> a
                    .antMatchers("/", "/error", "/webjars/**").permitAll()
                    .anyRequest().authenticated()
                )
                .exceptionHandling(e -> e
                    .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
                )
                .oauth2Login();
            // @formatter:on
        }
    }
    

    也就是说我们只需要添加maven依赖以及继承WebSecurityConfigurerAdapter进行一些简单的配置,一个oauth2客户端应用就构建完成了。接下来按照指南上的步骤点击页面的github登录链接我们的页面就会跳转到github授权登录页,等待用户授权完成之后浏览器重定向到我们的callback URL最终请求user信息端点即可访问到刚刚登入的github用户信息,整个应用的构建是如此的简单,背后的原理是什么呢?接下来我们开始分析。还是和以前一样,我们在配置文件中将security的日志级别设置为debug

    logging:
      level:
        org.springframework.security: debug
    

    重新启动应用之后,从控制台输出中我们可以看到与普通spring-security应用不同的地方在于整个过滤链多出了以下几个过滤器

    OAuth2AuthorizationRequestRedirectFilter
    OAuth2LoginAuthenticationFilter
    

    联想oauth2的授权码模式以及这两个过滤器的名字,熟悉spring-security的同学心中肯定已经有了一点想法了。对没错,spring-security对客户端模式的支持完全就是基于这两个过滤器来实现的。现在我们来回想以下授权码模式的执行流程

    1. 用户在客户端页面点击三方应用登录按钮(客户端就是我们刚刚注册的github应用)

    2. 页面跳转到三方应用注册的授权方页面(授权服务器即github)

    3. 用户登入授权后,github调用我们应用的回调地址(我们刚刚注册github应用时填写的回调地址)

    4. 第三步的回调地址中github会将code参数放到url中,接下来我们的客户端就会在内部拿这个code再次去调用github的access_token地址获取令牌

    上面就是标准的authorization_code授权模式,OAuth2AuthorizationRequestRedirectFilter的作用就是上面步骤中的1.2步的合体,当用户点击页面的github授权url之后,OAuth2AuthorizationRequestRedirectFilter匹配这个请求,接着它会将我们配置文件中的clientId、scope以及构造一个state参数(防止csrf攻击)拼接成一个url重定向到github的授权url,OAuth2LoginAuthenticationFilter的作用则是上面3.4步骤的合体,当用户在github的授权页面授权之后github调用回调地址,OAuth2LoginAuthenticationFilter匹配这个回调地址,解析回调地址后的code与state参数进行验证之后内部拿着这个code远程调用github的access_token地址,拿到access_token之后通过OAuth2UserService获取相应的用户信息(内部是拿access_token远程调用github的用户信息端点)最后将用户信息构造成Authentication被SecurityContextPersistenceFilter过滤器保存到HttpSession中。下面我们就来看一下这两个过滤器内部执行的原理

    OAuth2AuthorizationRequestRedirectFilter

    public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
        
        ......省略部分代码
    
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
       throws ServletException, IOException {
    
      try {
       OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
       if (authorizationRequest != null) {
        this.sendRedirectForAuthorization(request, response, authorizationRequest);
        return;
       }
      } catch (Exception failed) {
       this.unsuccessfulRedirectForAuthorization(request, response, failed);
       return;
      }
            ......省略部分代码
    }
    

    通过authorizationRequestResolver解析器解析请求,解析器的默认实现是DefaultOAuth2AuthorizationRequestResolver,核心解析方法如下

    
    // 第一步解析
    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        // 通过内部的authorizationRequestMatcher来解析当前请求中的registrationId
        // 也就是/oauth2/authorization/github中的github
        String registrationId = this.resolveRegistrationId(request);
        String redirectUriAction = getAction(request, "login");
        return resolve(request, registrationId, redirectUriAction);
    }
    
    // 第二步解析
    private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) {
        if (registrationId == null) {
            return null;
        }
     // 根据传入的registrationId找到注册的应用信息
        ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
        if (clientRegistration == null) {
            throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
        }
    
        Map<String, Object> attributes = new HashMap<>();
        attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
    
        OAuth2AuthorizationRequest.Builder builder;
        // 根据不同的AuthorizationGrantType构造不同的builder
        if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
            builder = OAuth2AuthorizationRequest.authorizationCode();
            Map<String, Object> additionalParameters = new HashMap<>();
            if (!CollectionUtils.isEmpty(clientRegistration.getScopes()) &&
                clientRegistration.getScopes().contains(OidcScopes.OPENID)) {
                // Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
                // scope
                //   REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
                addNonceParameters(attributes, additionalParameters);
            }
            if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
                addPkceParameters(attributes, additionalParameters);
            }
            builder.additionalParameters(additionalParameters);
        } else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
            builder = OAuth2AuthorizationRequest.implicit();
        } else {
            throw new IllegalArgumentException("Invalid Authorization Grant Type ("  +
                                               clientRegistration.getAuthorizationGrantType().getValue() +
                                               ") for Client Registration with Id: " + clientRegistration.getRegistrationId());
        }
    
        String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
    
        OAuth2AuthorizationRequest authorizationRequest = builder
            .clientId(clientRegistration.getClientId())
            .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
            .redirectUri(redirectUriStr)
            .scopes(clientRegistration.getScopes())
            // 生成随机state值
            .state(this.stateGenerator.generateKey())
            .attributes(attributes)
            .build();
    
        return authorizationRequest;
    }
    

    DefaultOAuth2AuthorizationRequestResolver判断请求是否是授权请求,最终返回一个OAuth2AuthorizationRequest对象给OAuth2AuthorizationRequestRedirectFilter,如果OAuth2AuthorizationRequest不为null的话,说明当前请求是一个授权请求,那么接下来就要拿着这个请求重定向到授权服务器的授权端点了,下面我们接着看OAuth2AuthorizationRequestRedirectFilter发送重定向的逻辑

    private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
                                              OAuth2AuthorizationRequest authorizationRequest) throws IOException {
    
        if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {
            this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
        }
        this.authorizationRedirectStrategy.sendRedirect(request, response, authorizationRequest.getAuthorizationRequestUri());
    }
    
    1. 如果当前是授权码类型的授权请求那么就需要将这个请求信息保存下来,因为接下来授权服务器回调我们需要用到这个授权请求的参数进行校验等操作(比对state),这里是通过authorizationRequestRepository保存授权请求的,默认的保存方式是通过HttpSessionOAuth2AuthorizationRequestRepository保存在httpsession中的,具体的保存逻辑很简单,这里就不细说了。

    2. 保存完成之后就要开始重定向到授权服务端点了,这里默认的authorizationRedirectStrategy是DefaultRedirectStrategy,重定向的逻辑很简单,通过response.sendRedirect方法使前端页面重定向到指定的授权

    public void sendRedirect(HttpServletRequest request, HttpServletResponse response,
                             String url) throws IOException {
        String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
        redirectUrl = response.encodeRedirectURL(redirectUrl);
    
        if (logger.isDebugEnabled()) {
            logger.debug("Redirecting to '" + redirectUrl + "'");
        }
    
        response.sendRedirect(redirectUrl);
    }
    

    OAuth2AuthorizationRequestRedirectFilter处理逻辑讲完了,下面我们对它处理过程做一个总结

    1. 通过内部的OAuth2AuthorizationRequestResolver解析当前的请求,返回一个OAuth2AuthorizationRequest对象,如果当前请求是授权端点请求,那么就会返回一个构造好的对象,包含我们的client_id、state、redirect_uri参数,如果对象为null的话,那么就说明当前请求不是授权端点请求。注意如果OAuth2AuthorizationRequestResolver不为null的话,OAuth2AuthorizationRequestResolver内部会将其保存在httpsession中这样授权服务器在调用我们的回调地址时我们就能从httpsession中取出请求将state进行对比以防csrf攻击。

    2. 如果第一步返回的OAuth2AuthorizationRequest对象不为null的话,接下来就会通过response.sendRedirect的方法将OAuth2AuthorizationRequest中的授权端点请求发送到前端的响应头中然后浏览器就会重定向到授权页面,等待用户授权。

    OAuth2LoginAuthenticationFilter

    public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
        @Override
     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
       throws AuthenticationException {
    
      MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
            // 如果请求参数中没有state和code参数,说明当前请求是一个非法请求
      if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
       OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
       throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
      }
      // 从httpsession中取出OAuth2AuthorizationRequestRedirectFilter中保存的授权请求,
            // 如果找不到的话说明当前请求是非法请求
      OAuth2AuthorizationRequest authorizationRequest =
        this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
      if (authorizationRequest == null) {
       OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
       throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
      }
      
            // 如果当前注册的应用中找不到授权请求时的应用了,那么也是一个不正确的请求
      String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);
      ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
      if (clientRegistration == null) {
       OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,
         "Client Registration not found with Id: " + registrationId, null);
       throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
      }
      String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
        .replaceQuery(null)
        .build()
        .toUriString();
      OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
    
      Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
      OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(
        clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
      authenticationRequest.setDetails(authenticationDetails);
      
            // 将未认证的OAuth2LoginAuthenticationToken委托给AuthenticationManager
            // 选择合适的AuthenticationProvider来对其进行认证,这里的AuthenticationProvider是
            // OAuth2LoginAuthenticationProvider
      OAuth2LoginAuthenticationToken authenticationResult =
       (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);
      
            // 将最终的认证信息封装成OAuth2AuthenticationToken
      OAuth2AuthenticationToken oauth2Authentication = new OAuth2AuthenticationToken(
       authenticationResult.getPrincipal(),
       authenticationResult.getAuthorities(),
       authenticationResult.getClientRegistration().getRegistrationId());
      oauth2Authentication.setDetails(authenticationDetails);
      
            // 构造OAuth2AuthorizedClient,将所有经过授权的客户端信息保存起来,默认是通过
            // AuthenticatedPrincipalOAuth2AuthorizedClientRepository来保存的,
            // 然后就能通过其来获取之前所有已授权的client?暂时不能确定其合适的用途
      OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
       authenticationResult.getClientRegistration(),
       oauth2Authentication.getName(),
       authenticationResult.getAccessToken(),
       authenticationResult.getRefreshToken());
    
      this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
    
      return oauth2Authentication;
     }
    }
    

    OAuth2LoginAuthenticationFilter的作用很简单,就是响应授权服务器的回调地址,核心之处在于OAuth2LoginAuthenticationProvider对OAuth2LoginAuthenticationToken的认证。

    OAuth2LoginAuthenticationProvider

    public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
        
         ...省略部分代码
        
        @Override
     public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      OAuth2LoginAuthenticationToken authorizationCodeAuthentication =
       (OAuth2LoginAuthenticationToken) authentication;
    
      // Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
      // scope
      //   REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
      if (authorizationCodeAuthentication.getAuthorizationExchange()
       .getAuthorizationRequest().getScopes().contains("openid")) {
       // This is an OpenID Connect Authentication Request so return null
       // and let OidcAuthorizationCodeAuthenticationProvider handle it instead
       return null;
      }
    
      OAuth2AccessTokenResponse accessTokenResponse;
      try {
       OAuth2AuthorizationExchangeValidator.validate(
         authorizationCodeAuthentication.getAuthorizationExchange());
       // 远程调用授权服务器的access_token端点获取令牌
       accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
         new OAuth2AuthorizationCodeGrantRequest(
           authorizationCodeAuthentication.getClientRegistration(),
           authorizationCodeAuthentication.getAuthorizationExchange()));
    
      } catch (OAuth2AuthorizationException ex) {
       OAuth2Error oauth2Error = ex.getError();
       throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
      }
      
             
      OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();
      Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
      
             // 通过userService使用上一步拿到的accessToken远程调用授权服务器的用户信息
      OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
        authorizationCodeAuthentication.getClientRegistration(), accessToken, additionalParameters));
    
      Collection<? extends GrantedAuthority> mappedAuthorities =
       this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
      
             // 构造认证成功之后的认证信息
      OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
       authorizationCodeAuthentication.getClientRegistration(),
       authorizationCodeAuthentication.getAuthorizationExchange(),
       oauth2User,
       mappedAuthorities,
       accessToken,
       accessTokenResponse.getRefreshToken());
      authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
    
      return authenticationResult;
     }
        ...省略部分代码
    }
    

    OAuth2LoginAuthenticationProvider的执行逻辑很简单,首先通过code获取access_token,然后通过access_token获取用户信息,这和标准的oauth2授权码模式一致。

    自动配置

    在spring指南的例子中,我们发现只是配置了一个简单oauth2Login()方法,一个完整的oauth2授权流程就构建好了,其实这完全归功于spring-boot的autoconfigure,我们找到spring-boot-autoconfigure.jar包中的security.oauth2.client.servlet包,可以发现spring-boot给我们提供了几个自动配置类

    OAuth2ClientAutoConfiguration
    OAuth2ClientRegistrationRepositoryConfiguration
    OAuth2WebSecurityConfiguration
    

    其中OAuth2ClientAutoConfiguration导入了OAuth2ClientRegistrationRepositoryConfiguration和OAuth2WebSecurityConfiguration的配置

    OAuth2ClientRegistrationRepositoryConfiguration

    @Configuration(proxyBeanMethods = false)
    @EnableConfigurationProperties(OAuth2ClientProperties.class)
    @Conditional(ClientsConfiguredCondition.class)
    class OAuth2ClientRegistrationRepositoryConfiguration {
    
        @Bean
        @ConditionalOnMissingBean(ClientRegistrationRepository.class)
        InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) {
            List<ClientRegistration> registrations = new ArrayList<>(
                OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
            return new InMemoryClientRegistrationRepository(registrations);
        }
    
    }
    

    OAuth2ClientRegistrationRepositoryConfiguration将我们在配置文件中注册的client构造成ClientRegistration然后保存到内存之中。这里有一个隐藏的CommonOAuth2Provider类,这是一个枚举类,里面事先定义好了几种常用的三方登录授权服务器的各种参数例如GOOGLE、GITHUB、FACEBOO、OKTA

    CommonOAuth2Provider

    public enum CommonOAuth2Provider {
    
     GOOGLE {
    
      @Override
      public Builder getBuilder(String registrationId) {
       ClientRegistration.Builder builder = getBuilder(registrationId,
         ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
       builder.scope("openid", "profile", "email");
       builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
       builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
       builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");
       builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
       builder.userNameAttributeName(IdTokenClaimNames.SUB);
       builder.clientName("Google");
       return builder;
      }
     },
    
     GITHUB {
    
      @Override
      public Builder getBuilder(String registrationId) {
       ClientRegistration.Builder builder = getBuilder(registrationId,
         ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
       builder.scope("read:user");
       builder.authorizationUri("https://github.com/login/oauth/authorize");
       builder.tokenUri("https://github.com/login/oauth/access_token");
       builder.userInfoUri("https://api.github.com/user");
       builder.userNameAttributeName("id");
       builder.clientName("GitHub");
       return builder;
      }
     },
    
     FACEBOOK {
    
      @Override
      public Builder getBuilder(String registrationId) {
       ClientRegistration.Builder builder = getBuilder(registrationId,
         ClientAuthenticationMethod.POST, DEFAULT_REDIRECT_URL);
       builder.scope("public_profile", "email");
       builder.authorizationUri("https://www.facebook.com/v2.8/dialog/oauth");
       builder.tokenUri("https://graph.facebook.com/v2.8/oauth/access_token");
       builder.userInfoUri("https://graph.facebook.com/me?fields=id,name,email");
       builder.userNameAttributeName("id");
       builder.clientName("Facebook");
       return builder;
      }
     },
    
     OKTA {
    
      @Override
      public Builder getBuilder(String registrationId) {
       ClientRegistration.Builder builder = getBuilder(registrationId,
         ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
       builder.scope("openid", "profile", "email");
       builder.userNameAttributeName(IdTokenClaimNames.SUB);
       builder.clientName("Okta");
       return builder;
      }
     };
    
     private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}";
    
     protected final ClientRegistration.Builder getBuilder(String registrationId,
                   ClientAuthenticationMethod method, String redirectUri) {
      ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(registrationId);
      builder.clientAuthenticationMethod(method);
      builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
      builder.redirectUriTemplate(redirectUri);
      return builder;
     }
        
     public abstract ClientRegistration.Builder getBuilder(String registrationId);
    
    }
    

    这就是为什么我们没有配置github授权端点确能够跳转授权页面的原因。

    OAuth2WebSecurityConfiguration

    OAuth2WebSecurityConfiguration配置一些web相关的类,像如何去保存和获取已经授权过的客户端,以及默认的oauth2客户端相关的配置

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnBean(ClientRegistrationRepository.class)
    class OAuth2WebSecurityConfiguration {
    
     @Bean
     @ConditionalOnMissingBean
     OAuth2AuthorizedClientService authorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {
      return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
     }
    
     @Bean
     @ConditionalOnMissingBean
     OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) {
      return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
     }
     
        // 默认的oauth2客户端相关的配置
     @Configuration(proxyBeanMethods = false)
     @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
     static class OAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
    
      @Override
      protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
       http.oauth2Login(Customizer.withDefaults());
       http.oauth2Client();
      }
    
     }
    
    }
    推荐文章
    
    更多项目源码
    
    展开全文
  • SMTP: 简单邮件传输协议 (Simple Mail Transfer Protocol, SMTP) 是在Internet传输email的事实标准。负责底层的邮件系统将邮件从一台机器传至另一台机器 也就是邮件的发送。 POP3: 全名为“Post Office Protocol -...

    说起发送邮件我们经常听到SMTP、POP3、IMAP协议 他们在邮件发送过程中都做了些什么了,下面简单的介绍一下

    SMTP: 简单邮件传输协议 (Simple Mail Transfer Protocol, SMTP) 是在Internet传输email的事实标准。负责底层的邮件系统将邮件从一台机器传至另一台机器 也就是邮件的发送。

    POP3: 全名为“Post Office Protocol - Version 3”,即“邮局协议版本3”。是TCP/IP协议族中的一员,由RFC1939 定义。本协议主要用于支持使用客户端远程管理在服务器上的电子邮件。提供了SSL加密的POP3协议被称为POP3S。
    smtp发送邮件到服务器,电子邮件客户端调用邮件客户机程序以连接服务器,并下载所有未阅读的电子邮件。

    IMAP: 跟POP一样主要负责下载查看邮件等,IMAP协议克服了POP协议的一些缺点,优于pop协议

    SMTP是 “推” 的操作,POP3/IMAP 是 “拉” 的操作

    • 参考博客:

    https://blog.csdn.net/u012702547/article/details/79494474

    https://baijiahao.baidu.com/s?id=1590529429501808795&wfr=spider&for=pc

    • demo地址:

    https://github.com/pengliangs/my-spring-demo/tree/master/spring-boot-email

    启动项目访问:http://localhost:8080 查看当前的小demo

    在这里插入图片描述

    使用Spring Boot发送邮件

    这里使用的是qq邮箱进行作为邮箱服务器

    使用QQ邮箱准备工作

    1.进入qq邮箱进入邮箱设置

    在这里插入图片描述

    2.点击账户选项卡

    在这里插入图片描述

    3.找到POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务 开启 POP3/SMTP服务

    在这里插入图片描述

    https://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=1001256

    开始发送邮件

    1、添加依赖

    这里使用的版本是2.x

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
    

    2、添加配置参数

    spring:
      mail:
        host: smtp.qq.com #邮箱服务地址
        username: xxxx@qq.com #发送人
        password: wrbsddwshxwf #qq邮箱密码是发送方的授权码
        properties:
          # 启动SSL时的配置
          mail.smtp.socketFactory.class: javax.net.ssl.SSLSocketFactory
          mail.smtp.socketFactory.fallback: false
          mail.smtp.socketFactory.port: 465
    

    3、开始编码

    • 发送一个简单的文本邮件
    @Test
    public void sendSimpleEmailTest() {
        SimpleMailMessage msg = new SimpleMailMessage();
        //发送人
        msg.setFrom("1427985322@qq.com");
        //收件人
        msg.setTo("2874267468@qq.com");
        //主题
        msg.setSubject("测试邮件发送,sendSimpleEmailTest");
        //主题内容
        msg.setText("https://pengliangs.github.io");
        javaMailSender.send(msg);
    }
    
    • 发送一个带有html格式邮件
    @Test
    public void sendHtmlEmailTest() throws MessagingException {
        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true);
        helper.setFrom("1427985322@qq.com");
        helper.setTo("2440329859@qq.com");
        helper.setSubject("测试邮件发送,sendHtmlEmailTest");
        helper.setText("<div style='width:500px;height:500px;text-align: center;'>" +
                "<h1>我的个人博客</h1>" +
                "<p>https://pengliangs.github.io</p>" +
                "</div>", true);
        javaMailSender.send(message);
    }
    
    • 发送一个带有附件邮件
    @Test
    public void sendAnnexEmailTest() throws MessagingException {
        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true);
        helper.setFrom("1427985322@qq.com");
        helper.setTo("2440329859@qq.com");
        helper.setSubject("测试带附件的邮件");
        helper.setText("带附件的邮件内容");
    
        FileSystemResource file = new FileSystemResource(new File("src/main/resources/static/img/gg.jpg"));
        //加入邮件
        helper.addAttachment("图片.jpg", file);
        javaMailSender.send(message);
    }
    
    • 发送一个模板邮件
     @Test
    public void sendTemplateMailTest() throws MessagingException, IOException, TemplateException {
        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true);
        helper.setFrom("1427985322@qq.com");
        helper.setTo("2440329859@qq.com");
        helper.setSubject("测试模板邮件");
    
        Map<String, Object> model = new HashMap<>();
        model.put("title", "来自远方的信件");
        model.put("content","这是我的博客:https://pengliangs.github.io");
        Template template = freeMarkerConfigurer.getConfiguration().getTemplate("email-template.html");
        String html = FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
        helper.setText(html, true);
        javaMailSender.send(message);
    }
    
    展开全文
  • 经常浏览技术社区、技术公众号的读者会有一个感受,那么就是 Spring Boot 相关的文章和相关咨询越来越多。包括小逛和技术公众号的博主交流,他们也发现推送 Spring Boot 相...
  • 前言在Kubernetes中部署Spring Boot应用整体上来说是一件比较繁琐的事情,而Spring Boot Operator[1]则能带给你清爽简单的体验。Spring Boot...
  • 1. 概述本文介绍在Spring Boot中实现对请求的数据进行校验。数据校验常用到概念: JSR303/JSR-349 1. 演示spring boot validation + 异常捕获机制实现数据自动校验功能 2. 自定义校验注解,并演示这个用法
  • Spring Boot 整合 Druid 概述 Druid 是阿里巴巴开源平台上的一个项目,整个项目由数据库连接池、插件框架和 SQL 解析器组成。该项目主要是为了扩展 JDBC 的一些限制,可以让程序员实现一些特殊的需求,比如向密钥...
  • 本文转自原文链接Spring SecuritySpring Security是Spring社区的一个顶级项目,也是Spring Boot官方推荐使用的Security框架。除了常规的Authentication和Authorization之外,Spring Security还提供了诸如ACLs,LDAP...
  • Spring Boot 安全框架 Spring Security 入门

    千次阅读 2020-04-30 02:19:15
    org.springframework.boot spring-boot-starter-parent 2.1.10.RELEASE 4.0.0 lab-01-springsecurity-demo org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-...
  • 1、首先我们需要在idea中创建spring boot项目,可以看我前面写到的博客:https://blog.csdn.net/qq_41790332/article/details/100065523 2、然后我们的项目结构如下: pom.xml文件中需要引进的依赖如下: <...
  • 主要内容:Spring Boot 2的基础应用、CORS配置、Actuator监控;Spring Boot集成springfox-swagger,利用Swagger生成JSON API文档,利用Swagger UI、Postman进行Rest API测试;Angular 6新特性,Angular与Spring Boot...
  • Spring Boot集成Spring Data Redis 生成项目模板 浏览器打开https://start.spring.io/ 添加依赖Spring Data Redis (Access+Driver) 引入依赖 pom.xml <?xml version="1.0" encoding="UTF-8"?> <project ...
  • Spring BootSpring data JPA简介

    万次阅读 2020-02-25 08:45:18
    Spring BootSpring data JPA简介 JPA的全称是Java Persistence API (JPA),他是一个存储API的标准,而Spring data JPA就是对JPA的一种实现,可以让我们方便的对数据进行存取。按照约定好的方法命名规则写dao层...
  • Spring boot整合mongodb

    2018-11-25 13:05:09
    Github源码:https://github.com/superRabbitMan/spring-boot-mongodb 本文基于spring-data-mongodb进行spring boot和mongodb的整合 引入依赖 <dependencies> <dependency> <group...
  • [Spring Boot] spring-boot-starter-data-jpa

    千次阅读 2019-04-27 22:21:54
    [Spring Boot] spring-boot-starter-data-jpa 单点登录实现准备工作(权限表设计和交互方式选择) 目录[Spring Boot] spring-boot-starter-data-jpa简介Spring Data JPA核心概念实战REFRENCES更多 手机用户请横屏...
  • 对于Git、Docker容器技术,其与Spring Boot的关联度非常的高,大部分的应用都是托管在GitHub上,然后,因为Spring Boot开发的应用大多服务与微服务架构,而这种架构通常依托与Docker实现快速部署和compose一键部署,...
  • 如今,借助于Spring Bootspring Data,spring和JPA集成已变得轻而易举。 我要设置一个PostgreSQL服务器 docker pull postgres #run the container docker run --name postgreslocal -e POSTGRES_PASSWORD=...
  • Spring Boot

    2019-08-13 16:21:21
    一、Spring Boot 入门 1、Spring Boot 简介 简化Spring应用开发的一个框架; 整个Spring技术栈的一个大整合; J2EE开发的一站式解决方案; 2、微服务 2014,martin fowler 微服务:架构风格(服务微化) 一个应用...
  • git clone https://github.com/callicoder/spring-boot-mysql-rest-api-tutorial.git 2.创建MySQL数据库 create database quartz_demo 3.根据您MySQL安装更改MySQL用户名和密码 打开src/main/resources/...
  • spring-boot-examples-源码

    2021-05-14 03:41:45
    Spring Boot 学习教程,示例代码 spring-boot-mongodb spring-boot-rocketmq spring-boot-redis spring-boot-rabbitmq spring-boot-kafka spring-boot-solr-cloud spring-boot-mybatis spring-boot-dubbo spring-...
  • Spring Boot 完整讲解

    万次阅读 多人点赞 2020-02-28 23:35:16
    文章目录SpringBoot学习笔记@[toc]一、 Spring Boot 入门预:必须掌握的技术:1. Spring Boot 简介背景解决的问题优点缺点2.微服务What are Microservices?单体应用微服务3.环境准备Maven设置:Idea设置4.Spring ...
  • Spring Boot+Vue+微人事视频教程本文地址:https://www.zyc.red/Spring/Security/OAuth2/OAuth2-Client/松哥在四月份出过一个 OAuth2 教程(公号后台回复 OAuth2 可以获取),里边也和大家分享了 GitHub 第三方登录,...
  • Spring Boot1.springboot介绍2.使用spring问题3.springboot特点4.springboot入门程序5.restful支持6.加载配置文件及常量信息7.springboot集成 spring-data-jpa8.springboot集成 mybatis9.springboot集成junit10....

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 5,696
精华内容 2,278
关键字:

bootemailgithubspring

spring 订阅