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

posted @ 2020-09-21 19:54  C3Stones  阅读(1185)  评论(0编辑  收藏  举报