Springboot Security5 Oauth2 集成Github、Facebook社交账号SSO登录

前言

最近有个项目是关于基于springboot oauth 整合Facebook、Twitter登陆,鉴于国内资料较少,将自己查阅的资料整理下,方便供大家参考

综述

1.Springboot
Springboot就是简化配置的spring,这里就不做详细描述了。
2.Spring Security5
Spring Security提供了基于Java EE的企业应用软件全面的安全服务。Springboot引入这个框架非常方便,我们在使用的时候,只要考虑三个地方就可以了:WebSecurityConfigureerAdapter 类的两个方法:
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
protected void configure(HttpSecurity http) throws Exception
还有UserDetailsService接口的方法:
public UserDetails loadUserByUsername(String username)
下来介绍这三个方法的作用。

public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
configureGlobal.png

这个方法的主要作用是用来如何验证用户账户密码,图中我们是用内存的user和password来验证的。(security5和security4的最大区别在于,验证需要传入new BCryptPassword()进行密码加密(支持自定义)在实际生产中,应该用数据库信息来验证,改为下图所示即可。


修改后的configureGlobal.png
protected void configure(HttpSecurity http) throws Exception

这个方法主要根据登陆的用户进行权限设置,进行验证请求。


configure.png

1.http.authorizeRequests()方法有多个子节点,每个macher按照他们的声明顺序执行(使用ant风格url匹配模式,?代表匹配任一个字符,* 代表匹配0个或者多个任意的字符,**代表匹配0个或者任意多个目录)。
2.我们指定任何用户都可以访问的多个URL模式。任何用户都可以访问URL以 "/resources/",开头的URL ,以及"/signup", "/about".
3.以"/admin/" 开头的URL只能由拥有 "ROLEADMIN"角色的用户访问. 请注意我们使用HasRole方法,没有使用ROLE前缀。
4.任何以/db/开头的URL需要用户同时具有"ROLEADMIN" 和 "ROLE_DBA". 和上面一样我们的hasRole方法也没有使用ROLE前缀。
5.尚未匹配的任何URL要求用户进行身份认证。
其他细节可以查看security中文文档
https://vincentmi.gitbooks.io/spring-security-reference-zh/content/3.4_authorize_requests.html

public UserDetails loadUserByUsername(String username)

这个方法源自接口package org.springframework.security.core.userdetails;需要自定义验证用户账户密码的时候就需要实现这个方法。
传入用户username,根据username到自己的数据库查询到用户的username,password和roles,生成一个新的UserDetails对象,将这个对象返回,security会将这个对象和登陆时填入的账号密码进行匹配,一致就会完成用户认证。


image.png

3.Oauth2.0
OAuth是一个关于授权的开放网络标准,在全世界得到的广泛的应用,目前是2.0的版本。OAuth2在“客户端”与“服务提供商”之间,设置了一个授权层(authorization layer)。“客户端”不能直接登录“服务提供商”,只能登录授权层,以此将用户与客户端分离。“客户端”登录需要OAuth提供的令牌,否则将提示认证失败而导致客户端无法访问服务。
OAuth2为我们提供了四种授权方式:

1、授权码模式(authorization code)
2、简化模式(implicit)
3、密码模式(resource owner password credentials)
4、客户端模式(client credentials)
这里facebook、Twitter、github、google均支持第一种模式,也是最安全的模式。
这里只要配置客户端,@EnableOAuth2Client就可以支持所有的社交平台,关键有一点,如果用户时第一次登陆,需要将用户信息注册到我们自己的数据库中,与数据库中的用户一一映射,就可以判断当前登陆的用户是对应我们数据库自己的平台身份。并且,Oauth2登陆时,我们将自定义的用户身份(也就是社交平台对应自己数据库的User)返回给security,进行登陆。这样就进行了用户绑定。这里主要实现了自定义的PrincipalExtractor接口,并非使用默认实现FixedPrincipalExtractor。参考FixedPrincipalExtractor的实现

FixedPrincipalExtractor.png

构建项目

这里从Idea的maven开始创建项目
maven依赖

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>cn.hinson</groupId>
  <artifactId>springboot_security5_oauth2</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>springboot_security5_oauth2</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <!-- 继承父包 -->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.1.RELEASE</version>
    <relativePath></relativePath>
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
    <mybatis.version>3.2.7</mybatis.version>
    <mybatis-spring.version>1.2.2</mybatis-spring.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.security.oauth.boot</groupId>
      <artifactId>spring-security-oauth2-autoconfigure</artifactId>
      <version>2.0.0.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.webjars</groupId>
      <artifactId>js-cookie</artifactId>
      <version>2.1.0</version>
    </dependency>
    <dependency>
      <groupId>org.webjars</groupId>
      <artifactId>jquery</artifactId>
      <version>2.1.1</version>
    </dependency>
    <dependency>
      <groupId>org.webjars</groupId>
      <artifactId>bootstrap</artifactId>
      <version>3.2.0</version>
    </dependency>
    <dependency>
      <groupId>org.webjars</groupId>
      <artifactId>webjars-locator-core</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
      <optional>true</optional>
    </dependency>

    <!--db-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.32</version>
    </dependency>

    <!--mybatis-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>${mybatis.version}</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>${mybatis-spring.version}</version>
    </dependency>
    <!-- mysql数据库连接池 pool -->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.0.15</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

导入maven依赖,建立项目包结构

包结构.png

下面展示核心的几个类实现代码:

SecurityConfig
Security5 和 Oauth2主要配置

package cn.hinson.security;

import cn.hinson.dao.UserDao;
import cn.hinson.security.oauth2.ClientResources;
import cn.hinson.security.service.MyUserDetailsService;
import cn.hinson.security.service.MyUserInfoTokenServices;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.filter.CompositeFilter;

import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.List;


@Configuration
@EnableOAuth2Client
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    OAuth2ClientContext oauth2ClientContext;

    @Bean
    UserDetailsService detailsService(){
        return new MyUserDetailsService();
    }



    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        Log logger = LogFactory.getLog(SecurityConfig.class);
        logger.info("HttpSecurity http");
        http.antMatcher("/**").authorizeRequests()
                .antMatchers("/", "/login**", "/webjars/**", "/test").permitAll()
                .anyRequest().authenticated().and().exceptionHandling()
                .and()
                    .logout().
                    logoutUrl("/logout").
                    logoutSuccessUrl("/")
                .and()
                .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
                .addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class)
                .formLogin();
        // @formatter:on
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

//        auth.inMemoryAuthentication()
//                .passwordEncoder(new BCryptPasswordEncoder())
//                .withUser("user1")
//                .password(new BCryptPasswordEncoder().encode("123456"))
//                .roles("USER");

        auth.userDetailsService(detailsService()).passwordEncoder(new BCryptPasswordEncoder());
    }


    @Bean
    public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
        FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<OAuth2ClientContextFilter>();
        registration.setFilter(filter);
        registration.setOrder(-100);
        return registration;
    }

    @Bean
    @ConfigurationProperties("github")
    public ClientResources github() {
        return new ClientResources("github");
    }

    @Bean
    @ConfigurationProperties("facebook")
    public ClientResources facebook() {
        return new ClientResources("facebook");
    }

    @Autowired
    GithubPrincipalExtractor githubPrincipalExtractor;

    @Autowired
    FacebookPrincipalExtractor facebookPrincipalExtractor;

    private Filter ssoFilter() {
        CompositeFilter filter = new CompositeFilter();
        List<Filter> filters = new ArrayList<>();
        filters.add(ssoFilter(facebook(), "/login/facebook", facebookPrincipalExtractor));
        filters.add(ssoFilter(github(), "/login/github", githubPrincipalExtractor));
        filter.setFilters(filters);
        return filter;
    }



    private Filter ssoFilter(ClientResources client, String path, AbstractPrincipalExtractor principalExtractor) {
        OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
                path);
        OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
        filter.setRestTemplate(template);
        UserInfoTokenServices tokenServices = new MyUserInfoTokenServices(client.getResource().getUserInfoUri(), client.getClient().getClientId(), principalExtractor);
        tokenServices.setRestTemplate(template);
        filter.setTokenServices(tokenServices);
        return filter;
    }
}

MyUserDetailsService
security 自定义认证,从数据库认证

package cn.hinson.security.service;

import cn.hinson.dao.UserDao;
import cn.hinson.domain.SysRole;
import cn.hinson.domain.SysUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.web.context.WebApplicationContext;

import java.util.ArrayList;
import java.util.List;

@Service
public class MyUserDetailsService implements UserDetailsService { //自定义UserDetailsService 接口

    @Autowired
    UserDao userDao;

    @Override
    public UserDetails loadUserByUsername(String username) { //重写loadUserByUsername 方法获得 userdetails 类型用户
        System.out.println("loadUserByUserName: " + username);
        SysUser user = userDao.findByUserName(username);
        if(user == null){
            throw new UsernameNotFoundException("用户名不存在");
        }
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        //用于添加用户的权限。只要把用户权限添加到authorities。
        for(SysRole role:user.getRoles())
        {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
            System.out.println(role.getName());
        }
        UserDetails userDetails = new User(user.getUsername(), user.getPassword(), authorities);
        return userDetails;
    }
}

AbstractPrincipalExtractor
负责社交账号与自己的数据库绑定

package cn.hinson.security;

import cn.hinson.domain.SysRole;
import cn.hinson.domain.SysUser;
import cn.hinson.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public abstract class AbstractPrincipalExtractor implements PrincipalExtractor {

  @Autowired
  UserService userService;
  //用户openid
  public abstract SysUser getUserByOpenId(String id);
  //用户角色,用“FACEBOOK"代表facebook用户,”GITHUB"代表"github用户
  public abstract SysRole getUserRoleByOauth2ClientName();

  @Override
  public Object extractPrincipal(Map<String, Object> map) {
    //得到对于的社交平台的openid
    String id =  map.get("id").toString();
    // Check if we've already registered this uer
    System.out.println("id: " + id);
    SysUser user = getUserByOpenId(id);
    if (user == null) {
      // If we haven't registered this user yet, create a new one
//      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//      // This Details object exposes a token that allows us to interact with Facebook on this user's behalf
//      String token = ((OAuth2AuthenticationDetails) authentication.getDetails()).getTokenValue();
      user = new SysUser();
      user.setUsername(map.get("id").toString());
      user.setGithubId(id);
      // Set the default Roles for users registered via Facebook
      List<SysRole> authorities = new ArrayList<>();
      SysRole role = new SysRole();
      role.setName("USER");
      authorities.add(role);
      //Oauth2Client客戶端特有角色
      authorities.add(getUserRoleByOauth2ClientName());
      user.setRoles(authorities);
      userService.createUser(user);
    }
    return user;
  }
}

增加其他社交平台
application.yml
在在application.yml中添加yml,然后在SecurityConfig中增加新的OauthClient,最后添加继承AbstractPrincipalExtractor的实现类(例如FacebookPrincipalExtractor类)即可

facebook:
  client:
    clientId: 233668646673605
    clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
    accessTokenUri: https://graph.facebook.com/oauth/access_token
    userAuthorizationUri: https://www.facebook.com/dialog/oauth
    tokenName: oauth_token
    authenticationScheme: query
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://graph.facebook.com/me
github:
  client:
    clientId: bd1c0a783ccdd1c9b9e4
    clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
    accessTokenUri: https://github.com/login/oauth/access_token
    userAuthorizationUri: https://github.com/login/oauth/authorize
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://api.github.com/user
package cn.hinson.security;

import cn.hinson.domain.SysRole;
import cn.hinson.domain.SysUser;
import org.springframework.stereotype.Component;

@Component
public class FacebookPrincipalExtractor extends AbstractPrincipalExtractor {

    @Override
    public SysUser getUserByOpenId(String id) {
        return super.userService.getUserByFacebookId(id);
    }

    @Override
    public SysRole getUserRoleByOauth2ClientName() {
        SysRole role = new SysRole();
        role.setName("FACEBOOK");
        return role;
    }
}

数据库

sql

CREATE TABLE sys_user (
  id            int  auto_increment PRIMARY KEY,
  username      VARCHAR (80),
  password      VARCHAR (80),
  twitterid     VARCHAR (30),
  facebookid    VARCHAR (30),
  githubid      VARCHAR (30)
);
CREATE TABLE sys_role (
  id            int auto_increment primary key,
  name varchar(80)
);
create table sys_role_user(
  id int auto_increment primary key,
  sys_user_id int,
  sys_role_id int,
  foreign key(sys_user_id) references sys_user(id),
  foreign key(sys_role_id) references sys_role(id)
);
navicator.png

访问

/localhost:8080/login
security自带的登陆界面

login.png

/localhost:8080/
未登陆时的界面

未登陆.png

登陆后的界面
登陆后.png

localhost:8080/user
登陆后的可进入

user.png

其他

本人也是在慢慢学习中,如有错误还请原谅、敬请指出,谢谢!
源代码
github:https://github.com/HinsonHsu/springboot_security5_oauth2.0

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

推荐阅读更多精彩内容