前言
最近有个项目是关于基于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
这个方法的主要作用是用来如何验证用户账户密码,图中我们是用内存的user和password来验证的。(security5和security4的最大区别在于,验证需要传入new BCryptPassword()进行密码加密(支持自定义)在实际生产中,应该用数据库信息来验证,改为下图所示即可。
protected void configure(HttpSecurity http) throws Exception
这个方法主要根据登陆的用户进行权限设置,进行验证请求。
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会将这个对象和登陆时填入的账号密码进行匹配,一致就会完成用户认证。
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的实现
构建项目
这里从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依赖,建立项目包结构
下面展示核心的几个类实现代码:
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)
);
访问
/localhost:8080/login
security自带的登陆界面
/localhost:8080/
未登陆时的界面
登陆后的界面
localhost:8080/user
登陆后的可进入
其他
本人也是在慢慢学习中,如有错误还请原谅、敬请指出,谢谢!
源代码
github:https://github.com/HinsonHsu/springboot_security5_oauth2.0