1. 簡介
在之前博客:SpringBoot基於JustAuth實現第三方授權登錄 和 SpringBoot + Layui +Mybatis-plus實現簡單后台管理系統(內置安全過濾器)上改造,除了原始的用戶名和密碼登錄外,增加第三方登錄認證。
2. 改造流程
- 在登錄頁增加第三方系統登錄鏈接
- 第三方系統注冊應用,並記錄
API Key
和Secret Key
- 將
API Key
、Secret Key
和回調地址添加到系統配置文件 - 改造回調方法,判斷授權用戶與系統用戶是否綁定
- 若已綁定,則跳轉到首頁
- 若未綁定,則跳轉到綁定頁進行綁定,綁定完成后跳轉到首頁
3. 流程圖
4. 改造代碼
下載示例工程:spring-boot-justauth-demo 和 :spring-boot-layui-demo,以spring-boot-layui-demo為基礎,進行改造。
- 授權用戶表增加user_id字段,並在本系統數據庫中創建
DROP TABLE IF EXISTS `t_ja_user`;
CREATE TABLE `t_ja_user` (
`uuid` varchar(64) NOT NULL COMMENT '用戶第三方系統的唯一id',
`username` varchar(100) NULL DEFAULT NULL COMMENT '用戶名',
`nickname` varchar(100) NULL DEFAULT NULL COMMENT '用戶昵稱',
`avatar` varchar(255) NULL DEFAULT NULL COMMENT '用戶頭像',
`blog` varchar(255) NULL DEFAULT NULL COMMENT '用戶網址',
`company` varchar(50) NULL DEFAULT NULL COMMENT '所在公司',
`location` varchar(255) NULL DEFAULT NULL COMMENT '位置',
`email` varchar(50) NULL DEFAULT NULL COMMENT '用戶郵箱',
`gender` varchar(10) NULL DEFAULT NULL COMMENT '性別',
`remark` varchar(500) NULL DEFAULT NULL COMMENT '用戶備注(各平台中的用戶個人介紹)',
`source` varchar(20) NULL DEFAULT NULL COMMENT '用戶來源',
`user_id` int(0) NULL DEFAULT NULL COMMENT '系統用戶ID',
PRIMARY KEY (`uuid`) USING BTREE
) ENGINE = InnoDB COMMENT = '授權用戶';
- 將JustAuth授權用戶相關的Entity、Service、Service Impl、Mapper拷貝到系統,Entity添加userId屬性,並添加set/get方法
import java.io.Serializable;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
/**
* 授權用戶信息
*
* @author CL
*
*/
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "t_ja_user")
@EqualsAndHashCode(callSuper = false)
public class JustAuthUser extends AuthUser implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用戶第三方系統的唯一id。在調用方集成該組件時,可以用uuid + source唯一確定一個用戶
*/
@TableId(type = IdType.INPUT)
private String uuid;
/**
* 用戶授權的token信息
*/
@TableField(exist = false)
private AuthToken token;
/**
* 第三方平台返回的原始用戶信息
*/
@TableField(exist = false)
private JSONObject rawUserInfo;
/**
* 系統用戶ID
*/
@Setter
@Getter
private Integer userId;
/**
* 自定義構造函數
*
* @param authUser 授權成功后的用戶信息,根據授權平台的不同,獲取的數據完整性也不同
*/
public JustAuthUser(AuthUser authUser) {
super(authUser.getUuid(), authUser.getUsername(), authUser.getNickname(), authUser.getAvatar(),
authUser.getBlog(), authUser.getCompany(), authUser.getLocation(), authUser.getEmail(),
authUser.getRemark(), authUser.getGender(), authUser.getSource(), authUser.getToken(),
authUser.getRawUserInfo());
}
}
- 配置文件添加配置
- 修改端口為8443(與注冊應用時一致)
- 添加redis配置(若justauth.cache.type配置使用default,則忽略此配置)
- 將第三方系統認證相關配置拷貝到系統配置文件中,並修改相關配置
- 可參考以下配置內容
server:
port: 8443
servlet:
session:
timeout: 1800s
spring:
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/layuidemo?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
username: root
password: 123456
# redis:
# host: 127.0.0.1
# port: 6379
# password: 123456
# # 連接超時時間(記得添加單位,Duration)
# timeout: 2000ms
# # Redis默認情況下有16個分片,這里配置具體使用的分片
# database: 0
# lettuce:
# pool:
# # 連接池最大連接數(使用負值表示沒有限制) 默認 8
# maxActive: 8
# # 連接池中的最大空閑連接 默認 8
# maxIdle: 8
thymeleaf:
prefix: classpath:/view/
suffix: .html
encoding: UTF-8
servlet:
content-type: text/html
# 生產環境設置true
cache: false
# Mybatis-plus配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
global-config:
db-config:
id-type: AUTO
configuration:
# 打印sql
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 日志配置
logging:
level:
com.xkcoding: debug
# 第三方系統認證
justauth:
enabled: true
type:
BAIDU:
client-id: xxxxxx
client-secret: xxxxxx
redirect-uri: http://127.0.0.1:8443/oauth/baidu/callback
GITEE:
client-id: xxxxxx
client-secret: xxxxxx
redirect-uri: http://127.0.0.1:8443/oauth/gitee/callback
cache:
# 緩存類型(default-使用JustAuth內置的緩存、redis-使用Redis緩存、custom-自定義緩存)
type: default
# 緩存前綴,目前只對redis緩存生效,默認 JUSTAUTH::STATE::
prefix: 'JUATAUTH::STATE::'
# 超時時長,目前只對redis緩存生效,默認3分鍾
timeout: 3m
# 信息安全
security:
web:
excludes:
- /login
- /logout
- /oauth/**
- /images/**
- /jquery/**
- /layui/**
xss:
enable: true
excludes:
- /login
- /logout
- /images/*
- /jquery/*
- /layui/*
sql:
enable: true
excludes:
- /images/*
- /jquery/*
- /layui/*
csrf:
enable: true
excludes:
- 重構AuthController,修改回調方法,增加用戶綁定方法
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.c3stones.auth.entity.JustAuthUser;
import com.c3stones.auth.service.JustAuthUserService;
import com.c3stones.common.response.Response;
import com.c3stones.sys.entity.User;
import com.c3stones.sys.service.UserService;
import com.xkcoding.justauth.AuthRequestFactory;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.BCrypt;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthRequest;
import me.zhyd.oauth.utils.AuthStateUtils;
/**
* 授權Controller
*
* @author CL
*
*/
@Slf4j
@Controller
@RequestMapping("/oauth")
public class AuthController {
@Autowired
private AuthRequestFactory factory;
@Autowired
private JustAuthUserService justAuthUserService;
@Autowired
private UserService userService;
/**
* 登錄
*
* @param type 第三方系統類型,例如:gitee/baidu
* @param response
* @throws IOException
*/
@GetMapping(value = "/login/{type}")
public void login(@PathVariable String type, HttpServletResponse response) throws IOException {
AuthRequest authRequest = factory.get(type);
response.sendRedirect(authRequest.authorize(AuthStateUtils.createState()));
}
/**
* 登錄回調
*
* @param type 第三方系統類型,例如:gitee/baidu
* @param callback
* @return
*/
@SuppressWarnings("unchecked")
@RequestMapping(value = "/{type}/callback")
public String login(@PathVariable String type, AuthCallback callback, Model model, HttpSession session) {
AuthRequest authRequest = factory.get(type);
AuthResponse<AuthUser> response = authRequest.login(callback);
log.info("登錄回調 => {}", JSON.toJSONString(response));
if (response.ok()) {
JustAuthUser justAuthUser = new JustAuthUser(response.getData());
JustAuthUser queryJustAuthUser = justAuthUserService.getById(justAuthUser.getUuid());
// 無授權用戶或者該授權用戶與系統用戶無綁定關系
if (queryJustAuthUser == null || queryJustAuthUser.getUserId() == null) {
justAuthUserService.saveOrUpdate(justAuthUser);
model.addAttribute("justAuthUser", justAuthUser);
return "userBinder";
}
session.setAttribute("user", userService.getById(queryJustAuthUser.getUserId()));
return "redirect:/index";
}
return "error/403";
}
/**
* 授權用戶和系統用戶綁定
*
* @param uuid 授權用戶Uuid
* @param user 系統用戶
* @param session
* @return
*/
@RequestMapping(value = "/userBinder/{uuid}")
@ResponseBody
public Response<String> userBinder(@PathVariable String uuid, User user, HttpSession session) {
if (StrUtil.isBlank(user.getUsername()) || StrUtil.isBlank(user.getPassword())) {
return Response.error("用戶名稱或密碼不能為空");
}
boolean checkUserNameResult = userService.checkUserName(user.getUsername());
if (checkUserNameResult) {
return Response.error("用戶不存在,請輸入系統中已存在的用戶");
}
User queryUser = new User();
queryUser.setUsername(user.getUsername());
queryUser = userService.getOne(new QueryWrapper<>(queryUser));
if (queryUser == null || !StrUtil.equals(queryUser.getUsername(), user.getUsername())
|| !BCrypt.checkpw(user.getPassword(), queryUser.getPassword())) {
return Response.error("用戶名稱或密碼錯誤");
}
JustAuthUser justAuthUser = new JustAuthUser();
justAuthUser.setUuid(uuid);
justAuthUser.setUserId(queryUser.getId());
boolean update = justAuthUserService.updateById(justAuthUser);
log.info("授權用戶(uuid){} 與系統用戶(id)綁定 {}", uuid, queryUser.getId());
if (update) {
session.setAttribute("user", queryUser);
return Response.success("登錄成功");
}
return Response.error("綁定系統用戶異常");
}
}
- 登錄添加第三方系統鏈接
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>C3Stones</title>
<link th:href="@{/images/favicon.ico}" rel="icon">
<link th:href="@{/layui/css/layui.css}" rel="stylesheet" />
<link th:href="@{/layui/css/login.css}" rel="stylesheet" />
<link th:href="@{/layui/css/view.css}" rel="stylesheet" />
<script th:src="@{/layui/layui.all.js}"></script>
<script th:src="@{/jquery/jquery-2.1.4.min.js}"></script>
</head>
<body class="login-wrap">
<div class="login-container">
<form class="login-form pb10">
<div class="input-group text-center text-gray">
<h2>歡迎登錄</h2>
</div>
<div class="input-group">
<input type="text" id="username" class="input-field">
<label for="username" class="input-label">
<span class="label-title">用戶名</span>
</label>
</div>
<div class="input-group">
<input type="password" id="password" class="input-field">
<label for="password" class="input-label">
<span class="label-title">密碼</span>
</label>
</div>
<button type="button" class="login-button">登錄<i class="ai ai-enter"></i></button>
<div class="input-group text-center pt20 pl0 pr0">
<a th:href="@{/oauth/login/gitee}"><span class="icon-gitee"></span></a>
<a th:href="@{/oauth/login/baidu}"><span class="icon-baidu"></span></a>
<a href="javascript:" class="disabled"><span class="icon-qq"></span></a>
<a href="javascript:" class="disabled"><span class="icon-github"></span></a>
</div>
</form>
</div>
</body>
</html>
<script>
layui.define(['element'],function(exports){
var $ = layui.$;
$('.input-field').on('change',function(){
var $this = $(this),
value = $.trim($this.val()),
$parent = $this.parent();
if(!isEmpty(value)){
$parent.addClass('field-focus');
}else{
$parent.removeClass('field-focus');
}
})
exports('login');
});
// 登錄
var layer = layui.layer;
$(".login-button").click(function() {
var username = $("#username").val();
var password = $("#password").val();
if (isEmpty(username) || isEmpty(password)) {
layer.msg("用戶名或密碼不能為空", {icon: 2});
return ;
}
var loading = layer.load(1, {shade: [0.3, '#fff']});
$.ajax({
url : "[[@{/}]]login",
data : {username : username, password : password},
type : "post",
dataType : "json",
error : function(data) {
},
success : function(data) {
layer.close(loading);
if (data.code == 200) {
location.href = "[[@{/}]]index";
} else {
layer.msg(data.msg, {icon: 2});
}
}
});
});
function isEmpty(n) {
if (n == null || n == '' || typeof(n) == 'undefined') {
return true;
}
return false;
}
</script>
- 在resource/view目錄下,新增userBinder.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>C3Stones</title>
<link th:href="@{/images/favicon.ico}" rel="icon">
<link th:href="@{/layui/css/layui.css}" rel="stylesheet" />
<link th:href="@{/layui/css/login.css}" rel="stylesheet" />
<link th:href="@{/layui/css/view.css}" rel="stylesheet" />
<script th:src="@{/layui/layui.all.js}"></script>
<script th:src="@{/jquery/jquery-2.1.4.min.js}"></script>
</head>
<body class="login-wrap">
<div class="login-container">
<form class="login-form">
<input type="hidden" id="uuid" th:value="${justAuthUser?.uuid}"/>
<div class="input-group text-center text-gray">
<h2>歡迎<b class="text-orange"> [[${justAuthUser?.nickname}]] </b>登錄</h2>
</div>
<div class="input-group">
<input type="text" id="username" class="input-field">
<label for="username" class="input-label">
<span class="label-title">用戶名</span>
</label>
</div>
<div class="input-group">
<input type="password" id="password" class="input-field">
<label for="password" class="input-label">
<span class="label-title">密碼</span>
</label>
</div>
<button type="button" class="login-button">登錄<i class="ai ai-enter"></i></button>
</form>
</div>
</body>
</html>
<script>
layui.define(['element'],function(exports){
var $ = layui.$;
$('.input-field').on('change',function(){
var $this = $(this),
value = $.trim($this.val()),
$parent = $this.parent();
if(!isEmpty(value)){
$parent.addClass('field-focus');
}else{
$parent.removeClass('field-focus');
}
})
exports('login');
});
// 登錄
var layer = layui.layer;
$(".login-button").click(function() {
var uuid = $("#uuid").val();
var username = $("#username").val();
var password = $("#password").val();
if (isEmpty(username) || isEmpty(password)) {
layer.msg("用戶名或密碼不能為空", {icon: 2});
return ;
}
var loading = layer.load(1, {shade: [0.3, '#fff']});
$.ajax({
url : "[[@{/}]]oauth/userBinder/" + uuid,
data : {username : username, password : password},
type : "post",
dataType : "json",
error : function(data) {
},
success : function(data) {
layer.close(loading);
if (data.code == 200) {
location.href = "[[@{/}]]index";
} else {
layer.msg(data.msg, {icon: 2});
}
}
});
});
function isEmpty(n) {
if (n == null || n == '' || typeof(n) == 'undefined') {
return true;
}
return false;
}
</script>
5. 測試
- 登錄
瀏覽器訪問:http://127.0.0.1:8443 。 - 跳轉到第三方系統登錄
點擊下方碼雲圖標,使用碼雲賬號登錄(前提已在碼雲創建應用)。 - 綁定系統用戶
第一次授權用戶未與系統用戶綁定,則跳轉到綁定頁面,輸入系統存在的用戶信息(user/123456),即可完成綁定。完成后跳轉到首頁。 - 退出,再一次測試登錄
若登錄的賬號已存在綁定關系,則在第三方認證通過后直接調整到首頁