基于oAuth2.0开发属于自己的SSO授权服务 - 授权码(Authourization Code)模式 (持续更新中。。。)

此文章篇幅较长,平日上班较少时间写作,请见谅。持续更新中。。。


oAuth2.0系列文章目录

oAuth2.0系列文章目录
一. 基于oAuth2.0开发属于自己的单点登录SSO授权服务 - 授权码Authourization Code模式

基于oAuth2.0开发属于自己的单点登录SSO授权服务 - 授权码Authourization Code模式 .
应用场景
开发环境
客户端应用和授权服务器端属于同一个集团, 处于不同微服务Micro Service内
后端: Java8, Spring Boot, Spring Security, Spring AOP, BouncyCastle, MySQL ..
前端: Vue 3, Element, Axios, Router
其他: Nginx反向代理, 国密SM2, SM4加密算法 ..

前言

本文用最基本的Spring Security,Spring Boot,不采用Spring Boot oAuth相关插件,重写oAuth2.0 SSO授权服务器。应用场景:客户端应用和授权服务器端属于同一个集团,处于不同微服务内;授权服务器和资源服务器处于相同服务器内。

一、具体实现及流程

用户在授权服务器注册用户(Happy Flow)

客户 客户端 授权服务器 数据库 访问客户端页面,发起SSO注册请求 1 获取 csrfToken, jSessionId,sm4salt 2 返回 csrfToken, jSessionId,sm4salt 3 利用sms短信验证码 / 电邮验证码,验证用户手机号码 / 电邮地址 4 利用bcrypt加密用户密码 5 利用SM4加密算法和自定义方式组合csrfToken, sm4salt生成密码 6 发起用户注册请求 7 loop [pre-registration] 验证客户端安全信息(appId,appSecret,callback url,scope等等) 8 验证成功,写入用户 9 返回注册结果 10 注册成功 11 客户 客户端 授权服务器 数据库

用户登录SSO(Happy Flow)

客户 客户端 授权服务器 资源服务器 数据库 访问客户端页面,发起SSO登录请求 1 获取 csrfToken, jSessionId,sm4salt 2 返回 csrfToken, jSessionId,sm4salt 3 发起登录请求 4 验证客户端用户安全信息(appId,appSecret,callback url,scope等等) 5 loop 验证用户 6 查询及验证用户 7 用户验证成功 8 根据callback url值,发起请求并返回用户信息、角色及相关权限 9 写入用户登录状态及用户权限到sessionStorage 10 loop 客户 客户端 授权服务器 资源服务器 数据库

二、具体实现步骤

1. 项目设置

1.1 引入Maven依赖库

<!-- Core Functions Dependencies -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
    <scope>provided</scope>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!-- Web Modules Dependencies -->
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-juli</artifactId>
    <version>${tomcat.version}</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>bootstrap</artifactId>
	<version>5.1.3</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>jquery</artifactId>
	<version>3.6.0</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>jquery-ui</artifactId>
	<version>1.13.0</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>font-awesome</artifactId>
	<version>5.15.4</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>webjars-locator-core</artifactId>
</dependency>

<!-- DB and Data Dependencies -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>	
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<scope>runtime</scope>
</dependency>
<!-- END Spring Boot default jdbc connector -->

<!-- Other Functions Dependencies -->
<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<optional>true</optional>
</dependency>

1.2 设置application.yml 和 application.properties

- application.yml
server:
  port: 12911
  servlet:
    context-path: /oauth2
spring:
  profiles:
    active:
    - default
  application:
    name: hp-auth-oauth2-sso-api-12911
- application.properties
#spring.datasource.url=jdbc:mysql://192.168.31.54:3306/PRIVILEGE?useSSL=false&useUnicode=true&characterEncoding=utf8
spring.datasource.url=jdbc:mysql://[your domain name]:3306/[your db name]?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
spring.datasource.username=[your db username]
spring.datasource.password=[your db password]
# Hikari will use the above plus the following to setup connection pooling
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=DatebookHikariCP
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1
#"none", This is the default for MySQL, no change to the database structure.
#update Hibernate changes the database according to the given Entity structures.
#create Creates the database every time, but don't drop it when close.
#create-drop Creates the database then drops it when the SessionFactory closes.
#It is good security practice that after your database is in production state, 
#  you make this none and revoke all privileges from the MySQL user connected to the Spring application, 
#  then give him only SELECT, UPDATE, INSERT, DELETE.
spring.jpa.hibernate.ddl-auto=none
spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.show-sql=true
#Unfortunately, Open Session in View (OSIV) is enabled by default in Spring Boot.
#So, make sure that setting: "spring.jpa.open-in-view=false"
#so that you can handle the LazyInitializationException the right way.
spring.jpa.open-in-view=false

# Hibernate properties
spring.jpa.properties.hibernate.id.new_generator_mappings=false

#thymelead properties
spring.mvc.static-path-pattern=/static/**
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.servlet.content-type=text/html
spring.thymeleaf.cache=false
spring.thymeleaf.enabled=true

2. 后端开发

2.1 创建Login Controller

package com.hivesplace.templates.controllers;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import com.hivesplace.templates.VOs.CsrfTokenJSessionIdVO;
import com.hivesplace.templates.beans.CsrfTokenJSessionId;

@Controller
public class LoginController {
	@Autowired
	CsrfTokenJSessionIdVO csrfTokenJSessionIdVO;
		
	@GetMapping(path="/login")
	public String login(HttpServletRequest request) {
		csrfTokenJSessionIdVO.setCsrfToken(new CsrfTokenJSessionId().getCsrfToken());
		csrfTokenJSessionIdVO.setJSessionId(request.getSession().getId());
		
		System.out.println(">>>>>>>>>>> csrfTokenJSessionVO: "+csrfTokenJSessionIdVO.toString());
		
		return "login";
	}
}

2.2 创建POJO - CsrfTokenJSessionId

package com.hivesplace.templates.beans;

import java.util.Random;

import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.stereotype.Component;

import lombok.Data;

@Data
@Component
public class CsrfTokenJSessionId {
	
	public CsrfTokenJSessionId() {
		this.setCsrfToken();
		this.jSessionId = "[please provide jSessionId by calling setter function - setJSessionId(String)]";
	}
	
	public CsrfTokenJSessionId(String csrfToken, String jSessionId) {
		this.csrfToken = csrfToken;
		this.jSessionId = jSessionId;
	}
	
	private String csrfToken;
	private String jSessionId;
	
	public void setCsrfToken() {
		final boolean USELETTERS = true;
		final boolean USENUMBERS = true;
		final int BEGININDEX = new Random().nextInt(32);
		final int ENDINDEX = BEGININDEX+32;
		
		this.csrfToken = RandomStringUtils.random(64, USELETTERS, USENUMBERS).substring(BEGININDEX, ENDINDEX);
	}
}

2.3 创建index.html (基于Webjars Bootstrap and jQuery)

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="icon" href="/oauth2/static/img/favicon.ico">

    <title>HIVESPLACE | Login</title>

    <!-- Bootstrap core CSS -->
    <link href="/oauth2/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">

    <!-- Custom styles for this template -->
    <link href="/oauth2/static/css/signin.css" rel="stylesheet">
    <link href="/oauth2/static/css/corporate.css" rel="stylesheet">

</head>

<body class="text-center">

    <header class="display-none" id="loginWrap_header">
        <div class="corporate__header">
          <div class="header__wrapper">
            <div class="header__item">
              <nav class="top-bar">
                <span>
                    <div class="top-bar__holder">
                    <a href="#" target="_blank"><span class="top-bar__txt">HIVESPLACE</span></a>
                    </div>
                    <div class="top-bar__holder">
                    <a href="#" target="_blank"><span class="top-bar__txt">About HIVESPLACE</span></a>
                    </div>
                    <div class="top-bar__holder">
                    </div>
                </span>
              </nav>
            </div>
          </div>
        </div>
    </header>

    <div class="form-container row display-none" id="loginWrap_form">
        <div class="form-head-container my-auto">
            <div class="form-head">
                <img id="login-logo" class="mb-4" src="/oauth2/static/img/logoBee320x346.png" alt="">
                <h1 class="h3 mb-3 font-weight-normal">SSO Login</h1>
            </div>
            <div class="all-forms row">
                <form class="form-signin needs-validation" method="post" action="oauth-login" novalidate>
                    <div class="uname-pword">
                        <p>请输入用户名及密码</p>
                        <input type="text" id="inputUsername" name="username" class="form-control login-uname" placeholder="Username" autocomplete="username" required>
                        <div class="form-group pass_show"> 
                            <input type="password" id="inputPassword" name="password" class="form-control" placeholder="Password" autocomplete="current-password" required>
                            <span class="ptxt" id="showPassword">Show</span>
                        </div> 
                        <button class="btn btn-lg btn-primary btn-block mt-5 btn-submit" id="autoSignIn" type="submit">登入</button>
                    </div>
                </form>
                <div style="clear:both;"></div>
            </div>
        </div>
    </div>

    <!-- Footer -->
    <footer class="sticky-footer bg-white display-none" id="loginWrap_footer">
      <div class="container my-auto">
        <div class="copyright text-center my-auto">
           <span>版权 &copy; 2021 HIVESPLACE.com</span>
        </div>
      </div>
    </footer>
    <!-- End of Footer -->

    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="/oauth2/webjars/jquery/jquery.min.js"></script>
    <script src="/oauth2/webjars/bootstrap/js/bootstrap.min.js"></script>
    <script src="/oauth2/static/js/signin.js"></script>
</body>
</html>
signin.js
// Example starter JavaScript for disabling form submissions if there are invalid fields
  (function() {
    'use strict';
    window.addEventListener('load', function() {
      // Fetch all the forms we want to apply custom Bootstrap validation styles to
      var forms = document.getElementsByClassName('needs-validation');
      // Loop over them and prevent submission
      var validation = Array.prototype.filter.call(forms, function(form) {
        form.addEventListener('submit', function(event) {
          if (form.checkValidity() === false) {
            event.preventDefault();
            event.stopPropagation();
          }
          form.classList.add('was-validated');
        }, false);
      });
    }, false);
    
    //if(sessionStorage.autoLogin!=1){
		//console.log("session storage - username: "+sessionStorage.autoUsername);
    	$("#loginWrap_header").removeClass("display-none");
    	$("#loginWrap_form").removeClass("display-none");
    	$("#loginWrap_footer").removeClass("display-none");
    /*}else{
    	console.log("session storage - username: "+sessionStorage.autoUsername);
    	$("#inputUsername").val(sessionStorage.autoUsername);
    	$("#inputPassword").val(sessionStorage.autoPassword);
    	$("#autoSignIn").click();
    }*/
    
  })();

// jQuery for show password function
$('#showPassword').click(function(e){ 
    /*
    jQuery 使用函数来设置内容
	语法:		$(selector).attr(attribute,function(index,oldvalue))
    */
    $(this).text($(this).text() == "Show" ? "Hide" : "Show");
    /*
    jQuery 使用函数来设置属性/值
	语法:		$(selector).attr(attribute,function(index,oldvalue))
    */
    $(this).prev().attr('type', function(index, attr){return attr == 'password' ? 'text' : 'password'; }); 
});

// Stop redirect after validation pass & switch to SMS validation
var wrapWidth = $(".form-container").outerWidth();

总结

以上就是今天所讲的全部内容。

源代码

关注我并发表不少于10字评论可以获取本文源代码。

码农经典语录

Linus Torvalds
Talk is cheap, show me the code.

蜂窝码农

  • DRY Principle (Don’t Repeat Yourself) 是做技术的最大笑话, DRY Principle应该改成 DORY Principle (Do Repeat Yourself)才对
  • 没有中国参与的标准,不能称为世界标准

俗语
好读书、好记性不如烂笔头

参考链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值