QQ第三方登录文档 http://wiki.connect.qq/
一:成为个人开发者
1. 在QQ互联(https://connect.qq)上称为个人开发者,填上一些基本信息
- 名称: 自己的身份证上的姓名
- 联系地址:自己实际居住的地址
- 手机号码:自己的手机号
- 电子邮箱:自己的邮箱,申请开发者时会收到一封确认邮件"腾讯开放平台-开发者注册认证",点击一下链接就可以了
- 身份证号码:自己的身份证号码
- 照片:自己拿着身份证,并且身份证中的照片要在人的右边,具体如下图
信息都填完了,点击 提交审核,等待审核结果。
二:创建网站应用
创建网站应用时需要一个已经备过案的域名,可以通过IPC备案查询网站(http://icp.chinaz/) 查询域名的具体信息。如果自己没有备过案的域名,可以暂时使用公司的域名,这好像对公司没啥影响。如果没有公司域名,那就在网上随意找个域名,然后在IPC备案查询网站中查询一下该网站的具体信息,用别人的域名。如果用公司的或者别人的域名自己开发好功能了,删除掉应用,避免影响到别的公司,其实也没啥影响。
- 基本信息
- 网站名称 :通过 IPC备案查询网站查询的域名上会有网站名称,填写查询出来的名称
- 网站类别 :自己访问以下那个备案的网站,看一下该网站是什么类型的,然后选择一下
- 网站简介:随便写一下该网站的功能
- 网站logo:去网站上应该能找到相应的图片,下载下来,然后修改图片的大小100X100
- 平台信息
- 网站地址 :备案的域名,如http://www.example/
- 网站回调域: 备案域名的子路径,如http://www.example/social/callback 这个地址在第三方登录时会使用到,可以随便写,后面也可以随意修改
- 主办单位名称:在IPC备案查询网站中输入备案的域名可以查询出来
- 网站备案号:在IPC备案查询网站中输入备案的域名可以查询出来
三:社交登录原理
四:Spring Security接入
1. pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache/POM/4.0.0" xmlns:xsi="http://www.w3/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache/POM/4.0.0 http://maven.apache/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example.security</groupId>
<artifactId>springboot-security-social</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-security-social</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-web</artifactId>
<version>1.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-core</artifactId>
<version>1.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-config</artifactId>
<version>1.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-security</artifactId>
<version>1.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.0.14.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. AbstractOAuth2ApiBinding
调用QQ第三方的api获取用户的信息
public interface QQ {
QQUserInfo getUserInfo();
}
@Data
@ToString
@RequiredArgsConstructor
public class QQUserInfo {
/**
* 返回码
*/
private String ret;
/**
* 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。
*/
private String msg;
/**
*
*/
private String openId;
/**
* 不知道什么东西,文档上没写,但是实际api返回里有。
*/
private String is_lost;
/**
* 省(直辖市)
*/
private String province;
/**
* 市(直辖市区)
*/
private String city;
/**
* 出生年月
*/
private String year;
/**
* 用户在QQ空间的昵称。
*/
private String nickname;
/**
* 大小为30×30像素的QQ空间头像URL。
*/
private String figureurl;
/**
* 大小为50×50像素的QQ空间头像URL。
*/
private String figureurl_1;
/**
* 大小为100×100像素的QQ空间头像URL。
*/
private String figureurl_2;
/**
* 大小为40×40像素的QQ头像URL。
*/
private String figureurl_qq_1;
/**
* 大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100×100的头像,但40×40像素则是一定会有。
*/
private String figureurl_qq_2;
/**
* 性别。 如果获取不到则默认返回”男”
*/
private String gender;
/**
* 标识用户是否为黄钻用户(0:不是;1:是)。
*/
private String is_yellow_vip;
/**
* 标识用户是否为黄钻用户(0:不是;1:是)
*/
private String vip;
/**
* 黄钻等级
*/
private String yellow_vip_level;
/**
* 黄钻等级
*/
private String level;
/**
* 标识是否为年费黄钻用户(0:不是; 1:是)
*/
private String is_yellow_year_vip;
}
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
private static final String URL_GET_OPENID = "https://graph.qq/oauth2.0/me?access_token=%s";
private static final String URL_GET_USERINFO = "https://graph.qq/user/get_user_info?oauth_consumer_key=%s&openid=%s";
private String appId;
private String openId;
private ObjectMapper objectMapper = new ObjectMapper();
public QQImpl(String accessToken, String appId) {
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
this.appId = appId;
String url = String.format(URL_GET_OPENID, accessToken);
// callback( {"client_id":"xxx","openid":"xxxx"} );
String result = getRestTemplate().getForObject(url, String.class);
System.out.println(result);
String[] items = result.split("openid");
String openid = items[1].substring(3, items[1].length() - 6);
this.openId = openid;
}
/**
* QQ 互联
* https://connect.qq/ 文档资料 Api文档/Api列表/访问用户资料(get_user_info)
* @return
*/
public QQUserInfo getUserInfo() {
String url = String.format(URL_GET_USERINFO, appId, openId);
String result = getRestTemplate().getForObject(url, String.class);
System.out.println(result);
QQUserInfo userInfo = JSONObject.parseObject(result, QQUserInfo.class);
userInfo.setOpenId(openId);
return userInfo;
}
}
3. OAuth2Template
调用api发送请求的模板
public class QQOAuth2Template extends OAuth2Template {
public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
setUseParametersForClientAuthentication(true);
}
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
// access_token=xxx&expires_in=7776000&refresh_token=xxx
String responseString = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
String[] items = responseString.split("&");
String accessToken = items[0].split("=")[1];
Long expiresIn = new Long(items[1].split("=")[1]);
String refreshToen = items[2].split("=")[1];
return new AccessGrant(accessToken, null, refreshToen, expiresIn);
}
}
4. ApiAdapter
public class QQAdapter implements ApiAdapter<QQ> {
@Override
public boolean test(QQ qq) {
return true;
}
@Override
public void setConnectionValues(QQ qq, ConnectionValues values) {
QQUserInfo userInfo = qq.getUserInfo();
values.setDisplayName(userInfo.getNickname());
values.setImageUrl(userInfo.getFigureurl_qq_1());
values.setProfileUrl(null);
values.setProviderUserId(userInfo.getOpenId());
}
@Override
public UserProfile fetchUserProfile(QQ qq) {
return null;
}
@Override
public void updateStatus(QQ qq, String s) {
}
}
5. QQServiceProvider
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {
private String appId;
private static final String URL_AUTHORIZE = "https://graph.qq/oauth2.0/authorize";
private static final String URL_ACCESS_TOKEN = "https://graph.qq/oauth2.0/token";
public QQServiceProvider(String appId, String appSecret) {
super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
this.appId = appId;
}
@Override
public QQ getApi(String accessToken) {
return new QQImpl(accessToken, appId);
}
}
6. OAuth2ConnectionFactory
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
public QQConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
}
}
7. SocialConfiguration
@Data
@ConfigurationProperties(prefix = "social.qq")
public class QQProperties {
private String filterProcessesUrl = "/auth";
private String providerId = "qq";
private String appId;
private String appSecret;
private String signupUrl;
}
application.yml
注意:
- appId和appSecret要使用自己创建的应用对应的值。
- filterProcessesUrl和providerId 要和创建应用时网站回调域的子路径保持一致,即 如果网站回调域为
http://www.example/qqLogin/callback.do
,那么filterProcessesUrl为/qqLogin,而providerId为callback.do,关于网站回调域的子路径可以任意修改,修改这个值不需要QQ互联重新审核。
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root123
social:
qq:
appId: xxx
appSecret: xxx
filterProcessesUrl: /qqLogin
providerId: callback.do
signupUrl: /signup
ConnectionSignUp: 当第三方登录授权成功后跳转到signup页面,用于走注册流程或者完善账号信息流程(如手机号等信息)
@Component
public class MyConnecitonSignUp implements ConnectionSignUp {
@Override
public String execute(Connection<?> connection) {
// 根据社交用于信息默认创建用户并返回用户的唯一标识
return connection.getDisplayName();
}
}
public class MySpringSocialConfigurer extends SpringSocialConfigurer {
private String filterProcessesUrl;
public MySpringSocialConfigurer(String filterProcessesUrl) {
this.filterProcessesUrl = filterProcessesUrl;
}
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
return (T) filter;
}
}
注意:需要手动的先建好表,建表sql在 spring-social-core-1.1.4.RELEASE.jar/org/springframework/social/connect/jdbc/JdbcUsersConnectionRepository.sql
@Configuration
@EnableSocial
public class SocialConfiguration extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private QQProperties properties;
@Autowired
private MyConnecitonSignUp myConnecitonSignUp;
/**
* 建表 spring-social-core-1.1.4.RELEASE.jar/org/springframework/social/connect/jdbc/JdbcUsersConnectionRepository.sql
*
* create table UserConnection (userId varchar(255) not null,
* providerId varchar(255) not null,
* providerUserId varchar(255),
* rank int not null,
* displayName varchar(255),
* profileUrl varchar(512),
* imageUrl varchar(512),
* accessToken varchar(512) not null,
* secret varchar(512),
* refreshToken varchar(512),
* expireTime bigint,
* primary key (userId, providerId, providerUserId));
* create unique index UserConnectionRank on UserConnection(userId, providerId, rank);
* @param connectionFactoryLocator
* @return
*/
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
// Encryptors.noOpText() 对数据不加密
JdbcUsersConnectionRepository jdbcUsersConnectionRepository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
// 只能设置表前缀,但是不能修改表名
jdbcUsersConnectionRepository.setTablePrefix("");
if (myConnecitonSignUp != null) {
jdbcUsersConnectionRepository.setConnectionSignUp(myConnecitonSignUp);
}
return jdbcUsersConnectionRepository;
}
@Bean
public MySpringSocialConfigurer mySpringSocialConfigurer() {
MySpringSocialConfigurer springSocialConfigurer = new MySpringSocialConfigurer(properties.getFilterProcessesUrl());
springSocialConfigurer.userIdSource(getUserIdSource());
springSocialConfigurer.signupUrl(properties.getSignupUrl());
return springSocialConfigurer;
}
@Override
public UserIdSource getUserIdSource() {
return new AuthenticationNameUserIdSource();
}
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
return new ProviderSignInUtils(connectionFactoryLocator, getUsersConnectionRepository(connectionFactoryLocator));
}
}
@Configuration
@ConditionalOnProperty(prefix = "social.qq", name = "appId")
@EnableConfigurationProperties(QQProperties.class)
public class QQAutoConfig extends SocialConfigurerAdapter {
@Autowired
private QQProperties properties;
protected ConnectionFactory<?> createConnectionFactory() {
return new QQConnectionFactory(properties.getProviderId(), properties.getAppId(), properties.getAppSecret());
}
@Override
public void addConnectionFactories(ConnectionFactoryConfigurer configurer, Environment environment) {
configurer.addConnectionFactory(this.createConnectionFactory());
}
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
return null;
}
}
8. UserDetailsService
@Data
@ToString
@AllArgsConstructor
@RequiredArgsConstructor
public class SysPermission implements GrantedAuthority {
private Long id;
private String name;
private String code;
private String url;
private String method;
/**
* 次方法很重要,用于唯一标识一个权限
* @return
*/
@Override
public String getAuthority() {
return "ROLE_" + this.code + ":" + this.method.toUpperCase();
}
}
@Data
@ToString
@AllArgsConstructor
@RequiredArgsConstructor
public class SysUser {
private Long id;
private String username;
private String password;
private List<SysPermission> sysPermissions;
}
@Component
public class MyUserDetailsService implements UserDetailsService, SocialUserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询用户信息和权限
SysUser user = queryUserInfo(username, null);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new User(user.getUsername(), user.getPassword(), user.getSysPermissions());
}
@Override
public SocialUserDetails loadUserByUserId(String userId) throws UsernameNotFoundException {
// 根据用户名查询用户信息和权限
SysUser user = queryUserInfo(null, userId);
if (user == null) {
throw new UsernameNotFoundException(userId);
}
return new SocialUser(userId, user.getPassword(), true, true, true, true, user.getSysPermissions());
}
public SysUser queryUserInfo(String username, String userId) {
SysUser user = new SysUser(1L, "admin", "$2a$10$nm5H9QvnoWao.l7NbxQGZeZoR0Cn.VqCpsl3E/FhglPa954Zg9ccm", Arrays.asList(
new SysPermission(1L, "用户列表", "user:view", "/getUserList", "GET"),
new SysPermission(2L, "添加用户", "user:add", "/addUser", "POST"),
new SysPermission(3L, "修改用户", "user:update", "/updateUser", "PUT")
));
return user;
}
public static void main(String[] args) {
System.out.println(new BCryptPasswordEncoder().encode("123456"));
}
}
9. SecurityConfiguration
@Slf4j
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("login sucesssful {}", objectMapper.writeValueAsString(authentication));
response.sendRedirect("/index");
}
}
@Slf4j
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("认证失败");
response.setContentType("text/html;charset=utf-8");
response.getWriter().write(exception.getMessage());
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private MySpringSocialConfigurer mySpringSocialConfigurer;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.apply(mySpringSocialConfigurer).and()
// 配置需要认证的请求
.authorizeRequests()
.antMatchers("/login", "/signup", "/user/regist").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login")
.usernameParameter("username")
.passwordParameter("password")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
.failureUrl("/login?error")
.permitAll()
.and()
.logout()
.permitAll();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
10. UserController
@Data
public class SocialUserInfo {
private String providerId;
private String providerUserId;
private String nickname;
private String headimg;
}
@RestController
public class UserController {
@Autowired
private ProviderSignInUtils providerSignInUtils;
@GetMapping("/user/me")
public String me() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth.toString();
}
@GetMapping("/social/user")
public SocialUserInfo getSocialUserInfo(HttpServletRequest request) {
SocialUserInfo userInfo = new SocialUserInfo();
Connection<?> connection = providerSignInUtils.getConnectionFromSession(new ServletWebRequest(request));
userInfo.setProviderId(connection.getKey().getProviderId());
userInfo.setNickname(connection.getDisplayName());
userInfo.setHeadimg(connection.getImageUrl());
return userInfo;
}
@PostMapping("/user/regist")
public void regist(SysUser user, HttpServletRequest request) {
String userId = user.getUsername();
providerSignInUtils.doPostSignUp(userId, new ServletWebRequest(request));
}
}
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.addViewController("/signup").setViewName("signup");
registry.addViewController("/index").setViewName("index");
}
}
11. html
login.html
注意:<a href="/qqLogin/callback.do">QQ登录</a>
QQ登录的href地址为application.yml中的flterProcessesUrl和providerId的值的拼接在一起的子路径。
<!DOCTYPE html>
<html lang="en"
xmlns="http://www.w3/1999/xhtml">
<head>
<meta charset="utf-8">
<title>登录</title>
</head>
<body>
<a href="/qqLogin/callback.do">QQ登录</a>
</body>
</html>
signup.html
<!DOCTYPE html>
<html lang="en"
xmlns="http://www.w3/1999/xhtml">
<head>
<meta charset="utf-8">
<title>注册</title>
</head>
<body>
<form method="post" action="/user/regist">
<h2 class="form-signin-heading">注册</h2>
<p>
<label for="username">用户名</label>
<input type="text" id="username" name="username" required autofocus>
</p>
<p>
<label for="password">密码</label>
<input type="password" id="password" name="password" required>
</p>
<button type="submit" name="type" value="regist">注册</button>
<button type="submit" name="type" value="binding">绑定</button>
</form>
</body>
</html>
index.html
<!DOCTYPE html>
<html xmlns="http://www.w3/1999/xhtml"
xmlns:sec="http://www.thymeleaf/thymeleaf-extras-springsecurity4">
<head>
<meta charset="utf-8">
</head>
<body>
<h1>首页</h1>
登录名:<span sec:authentication="name"></span>
</body>
</html>
五:服务器配置
1. 配置hosts文件
vi /etc/hosts
# 这里的www.example为创建应用是填的网站地址对应的域名
127.0.0.1 www.example
2. nginx配置
主要是server配置,将80端口映射到8080端口上。QQ认证是会有个回调地址redirect_uri,这个地址默认会使用80端口,而我们的应用程序一般为其它端口,所以需要将80端口映射成8080端口。
注意在启用nginx时看一下80端口是否被占用了,像mac开机就会启动apache httpd就是用的80端口,需要将其停掉。查找80端口是否被占用命令 sudo lsof -n -P | grep :80
# 启动nginx
sudo nginx -c /usr/local/etc/nginx/nginx.conf
user root staff;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /usr/local/etc/nginx/logs/host.access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
gzip on;
server {
listen 80;
server_name www.example;
set $doc_root /usr/local/var/www;
location / {
proxy_pass http://www.example:8080;
}
location ^~ /images/ {
root $doc_root;
}
location ~ .(gif|jpg|jpeg|png|bmp|ico|swf|css|js)$ {
root $doc_root/img;
}
}
include servers/*;
}
访问登录页面使用域名来访问,如 http://www.example/login,登录成功进入到index.html
注意创建应用时的网站回调域的值要必须和QQ认证页面中的https://graph.qq/oauth2.0/show?redirect_uri=http://www.examole:8080/qqLogin/callback.do中的redirect_uri值要完全一致,这里不知道为什么redirect_uri中域名把8080端口给我带出来了,为了保持一致,我在配置网站回调域的时候也要把端口8080加上,这样才能保持完全一致。即网站回调域配置为http://www.examole:8080/qqLogin/callback.do
GitHub源码地址:https://github/mengday/springboot-security-social
更多推荐
Spring Security(六):认证(Authentication)-Web社交登录(Social)-QQ
发布评论