zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    awt
    登录校验 简单实现
    事务隔离级别
    事务的四大特性(ACID)
    多线程简单了解
    Eureka bug
    什么是存储过程
    filter和servlet的区别
    说说你对多线程锁机制的理解
    session的生命周期,session何时创建,何时销毁,session销毁的方式
  • 原文地址:https://www.cnblogs.com/cao-lei/p/13713604.html
Copyright © 2011-2022 走看看