SpringBoot + Layui + JustAuth +Mybatis-plus實現可第三方登錄的簡單后台管理系統


1. 簡介

  在之前博客:SpringBoot基於JustAuth實現第三方授權登錄SpringBoot + Layui +Mybatis-plus實現簡單后台管理系統(內置安全過濾器)上改造,除了原始的用戶名和密碼登錄外,增加第三方登錄認證。

2. 改造流程

  • 在登錄頁增加第三方系統登錄鏈接
  • 第三方系統注冊應用,並記錄API KeySecret Key
  • API KeySecret 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),即可完成綁定。完成后跳轉到首頁。
  • 退出,再一次測試登錄
      若登錄的賬號已存在綁定關系,則在第三方認證通過后直接調整到首頁

6. 項目地址

  spring-boot-layui-justauth-demo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM