SpringBoot + Layui + JustAuth +Mybatis-plus实现可第三方登录的简单后台管理系统
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),即可完成绑定。完成后跳转到首页。 - 退出,再一次测试登录
若登录的账号已存在绑定关系,则在第三方认证通过后直接调整到首页