Spring Boot2 使用 Spring Security + OAuth2 实现单点登录SSO

前言

目前系统都是比较流行的微服务架构,在企业发展初期,企业使用的系统很少,通常一个或者两个,每个系统都有自己的登录模块,使用人员每天用自己的账号登录,很方便。但随着企业的发展,用到的微服务系统随之增多,使用人员在操作不同的系统时,需要多次登录,而且每个系统的账号都不一样,都要记录,这对于使用人员来说,很不方便还不友好。于是,就想到是不是可以在一个统一登录门户平台登录,其他系统就不用登录了呢?这就是单点登录要解决的问题。
单点登录英文全称Single sign On,简称就是SSO。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。

技术简述

使用当下最流行的SpringBoot2技术,持久层使用MyBatis,权限控制Spring Security,数据库MySql,基于OAuth2认证授权协议,构建一个易理解、高可用、高扩展性的分布式单点登录应用基层。

OAuth2授权方式

OAuth2为我们提供了四种授权方式:

  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

授权码模式:授权码相对其他三种来说是功能比较完整、流程最安全严谨的授权方式,通过客户端的后台服务器与服务提供商的认证服务器交互来完成。

简化模式:这种模式不通过服务器端程序来完成,直接由浏览器发送请求获取令牌,令牌是完全暴露在浏览器中的,这种模式不太安全。

密码模式:密码模式也是比较常用到的一种,客户端向授权服务器提供用户名、密码然后得到授权令牌。这种模式不过有种弊端,我们的客户端需要存储用户输入的密码,但是对于用户来说信任度不高的平台是不可能让他们输入密码的。

客户端模式:客户端模式是客户端以自己的名义去授权服务器申请授权令牌,并不是完全意义上的授权。

上述简单的介绍了OAuth2内部的四种授权方式,我们下面使用授权码模式进行微服务单点登录实现,并且我们使用数据库存储用户登录信息、客户端授权信息。

一 授权服务

创建Spring Boot2项目,版本:2.3.0,项目名称:cloud-sso-upms

1.1 pom.xml

        <!-- oauth2 -->
		<dependency>
			<groupId>org.springframework.security.oauth.boot</groupId>
			<artifactId>spring-security-oauth2-autoconfigure</artifactId>
		</dependency>

		<!-- thymeleaf 模板引擎 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>

		<!-- spring security -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

1.2 application.properties

server.port=8087
server.servlet.context-path=/upms

#启用优雅关机
server.shutdown=graceful
#缓冲10秒
spring.lifecycle.timeout-per-shutdown-phase=10s

# mysql连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/upms?useUnicode=true&characterEncoding=UTF-8&userSSL=false&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
# druid web页面
druid.login.enabled=false
druid.login.username=druid
druid.login.password=druid
#druid连接池
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.initialSize=20
spring.datasource.minIdle=30
spring.datasource.maxActive=50
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 'x'
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=true
spring.datasource.testOnReturn=true
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
spring.datasource.filters=stat
spring.datasource.connectionProperties:druid.stat.slowSqlMillis=5000

# MyBatis 配置
mybatis.mapper-locations=classpath*:mapper/**/*Mapper.xml
# mybatis-plus 配置
mybatis-plus.mapper-locations=classpath*:mapper/**/*Mapper.xml
mybatis.configuration.jdbc-type-for-null=null

# thymeleaf 模板引擎配置
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.servlet.content-type=text/html
#开发时关闭缓存,不然没法看到实时页面
spring.thymeleaf.cache=false
#页面的存放路径就使用默认配置了
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.check-template-location=true
spring.thymeleaf.suffix=.html

1.3 数据库表

CREATE TABLE `oauth_client_details` (
  `client_id` VARCHAR(256) CHARACTER SET utf8 NOT NULL COMMENT '客户端唯一标识ID',
  `resource_ids` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL COMMENT '客户端所能访问的资源id集合',
  `client_secret` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL COMMENT '客户端访问密匙',
  `scope` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL COMMENT '客户端申请的权限范围',
  `authorized_grant_types` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL COMMENT '客户端授权类型',
  `web_server_redirect_uri` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL COMMENT '客户端的重定向URI',
  `authorities` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL COMMENT '客户端所拥有的权限值',
  `access_token_validity` INT(11) DEFAULT NULL COMMENT '客户端access_token的有效时间(单位:秒)',
  `refresh_token_validity` INT(11) DEFAULT NULL,
  `additional_information` VARCHAR(4096) CHARACTER SET utf8 DEFAULT NULL COMMENT '预留的字段',
  `autoapprove` VARCHAR(256) CHARACTER SET utf8 DEFAULT NULL COMMENT '是否跳过授权(true是,false否)',
  PRIMARY KEY (`client_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT='客户端授权表'

insert  into `oauth_client_details`
(`client_id`,`resource_ids`,`client_secret`,`scope`,`authorized_grant_types`,`web_server_redirect_uri`,`authorities`,`access_token_validity`,`refresh_token_validity`,`additional_information`,`autoapprove`) values 
('client1',NULL,'$2a$10$zLD4yC3sL.n58Fh52EN3C.CKloW6GN3QeJrNPfGaqotaH04M2Ssm6','all','authorization_code,refresh_token','http://localhost:8086/client1/login',NULL,7200,NULL,NULL,'true'),
('client2',NULL,'$2a$10$ZRmPFVgE6o2aoaK6hv49pOt5BZIKBDLywCaFkuAs6zYmRkpKHgyuO','all','authorization_code,refresh_token','http://localhost:8085/client2/login',NULL,7200,NULL,NULL,'true');

CREATE TABLE `sys_menu` (
  `menu_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `menu_name` VARCHAR(50) NOT NULL COMMENT '菜单名称',
  `menu_vice_name` VARCHAR(50) NOT NULL COMMENT '菜单副名称',
  `parent_id` BIGINT(20) DEFAULT '0' COMMENT '父菜单ID',
  `order_num` INT(4) DEFAULT '0' COMMENT '显示顺序',
  `url` VARCHAR(200) DEFAULT '#' COMMENT '请求地址',
  `menu_type` CHAR(1) DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
  `visible` CHAR(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
  `perms` VARCHAR(100) DEFAULT NULL COMMENT '权限标识',
  `icon` VARCHAR(100) DEFAULT '#' COMMENT '菜单图标',
  `create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者',
  `create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
  `update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者',
  `update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
  `remark` VARCHAR(500) DEFAULT '' COMMENT '备注',
  PRIMARY KEY (`menu_id`)
) ENGINE=INNODB AUTO_INCREMENT=1091 DEFAULT CHARSET=utf8 COMMENT='菜单权限表'

CREATE TABLE `sys_role` (
  `role_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_name` VARCHAR(30) NOT NULL COMMENT '角色名称',
  `role_key` VARCHAR(100) NOT NULL COMMENT '角色权限字符串',
  `role_sort` INT(4) NOT NULL COMMENT '显示顺序',
  `status` CHAR(1) NOT NULL COMMENT '角色状态(0正常 1停用 2删除)',
  `create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者',
  `create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
  `update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者',
  `update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`role_id`)
) ENGINE=INNODB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8 COMMENT='角色信息表'

CREATE TABLE `sys_role_menu` (
  `role_id` BIGINT(20) NOT NULL COMMENT '角色ID',
  `menu_id` BIGINT(20) NOT NULL COMMENT '菜单ID',
  PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='角色和菜单关联表'

CREATE TABLE `sys_user` (
  `user_id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `dept_id` BIGINT(20) DEFAULT NULL COMMENT '部门ID',
  `login_name` VARCHAR(30) NOT NULL COMMENT '登录名称',
  `user_name` VARCHAR(30) DEFAULT NULL COMMENT '用户名称',
  `email` VARCHAR(50) DEFAULT '' COMMENT '用户邮箱',
  `phone` VARCHAR(11) DEFAULT '' COMMENT '手机号码',
  `telephone` VARCHAR(12) DEFAULT '' COMMENT '座机号码',
  `duty` VARCHAR(30) DEFAULT '' COMMENT '职务',
  `sex` CHAR(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
  `avatar` VARCHAR(100) DEFAULT '' COMMENT '头像路径',
  `password` VARCHAR(100) DEFAULT '' COMMENT '密码',
  `salt` VARCHAR(20) DEFAULT '' COMMENT '盐加密',
  `status` CHAR(1) DEFAULT '0' COMMENT '状态(0正常 1停用 2删除)',
  `login_ip` VARCHAR(50) DEFAULT '' COMMENT '最后登陆IP',
  `login_date` DATETIME DEFAULT NULL COMMENT '最后登陆时间',
  `create_by` VARCHAR(64) DEFAULT '' COMMENT '创建者',
  `create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
  `update_by` VARCHAR(64) DEFAULT '' COMMENT '更新者',
  `update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`user_id`)
) ENGINE=INNODB AUTO_INCREMENT=61 DEFAULT CHARSET=utf8 COMMENT='用户信息表'

insert  into `sys_user`
(`user_id`,`dept_id`,`login_name`,`user_name`,`email`,`phone`,`telephone`,`duty`,`sex`,`avatar`,`password`,`salt`,`status`,`login_ip`,`login_date`,`create_by`,`create_time`,`update_by`,`update_time`,`remark`) values 
(1,1,'admin','admin','[email protected]','','','','0','','123','111','0','10.96.217.201','2021-02-25 11:15:51','',NULL,'',NULL,NULL);

CREATE TABLE `sys_user_role` (
  `user_id` BIGINT(20) NOT NULL COMMENT '用户ID',
  `role_id` BIGINT(20) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='用户和角色关联表'

1.4 代码实现

1.4.1 AuthorizationServerConfig客户端授权配置

package com.modules.common.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import javax.sql.DataSource;

/**
 *  客户端授权配置
 */
@Configuration
@EnableAuthorizationServer  //  开启授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    /**
     * 配置第三方应用,可以放在内存(inMemory),数据库
     * 四种授权模式("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
     * 1、授权码模式(authorization code)(正宗方式)(支持refresh token)
     * 2、密码模式(password)(为遗留系统设计)(支持refresh token)
     * 3、客户端模式(client_credentials)(为后台api服务消费者设计)(不支持refresh token)
     * 4、简化模式(implicit)(为web浏览器应用设计)(不支持refresh token)
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }

    /**
     * 需要暴露授权服务器端点
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.accessTokenConverter(jwtAccessTokenConverter());
        endpoints.tokenStore(jwtTokenStore());
//        endpoints.tokenServices(defaultTokenServices());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();
        security.tokenKeyAccess("isAuthenticated()");
    }


    /**
     * 配置TokenStore,有多种实现方式,redis,jwt,jdbc
     * @return
     */
    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("testKey");
        return converter;
    }
}

1.4.2 SpringSecurityConfig权限配置

package com.modules.common.config;

import com.modules.system.service.SysLoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 *  spring security配置
 * @author li'chao
 */
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {


    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private SysLoginService sysLoginService;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login")
                .and()
                .authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest()
                .authenticated()
                .and().csrf().disable().cors();
/*        http
                .requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**")
                .and()
                .authorizeRequests()
                .antMatchers("/oauth/**").authenticated()
                .and()
                .formLogin().permitAll();*/
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(sysLoginService).passwordEncoder(passwordEncoder());
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/assets/**", "/css/**", "/images/**");
    }

}

1.4.3 SysLoginService登录处理业务

package com.modules.system.service;

import com.baomidou.mybatisplus.toolkit.CollectionUtils;
import com.modules.system.entity.SysUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * 用户登录 业务层
 * @author li'chao
 *
 */
@Slf4j
@Component
public class SysLoginService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private SysMenuService sysMenuService;

    @Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
        // 根据登录名称查询用户信息
        SysUser sysUser = sysUserService.selectUserByLoginName(name);
        if (null == sysUser) {
            log.warn("用户{}不存在", name);
            throw new UsernameNotFoundException(name);
        }
        // 根据用户ID查询权限配置的菜单,获取菜单标识字段perms
        List<String> permsList = sysMenuService.selectPermsListByUserId(sysUser.getUserId());
        permsList.remove(null);
        List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
        if(!CollectionUtils.isEmpty(permsList)){
            for(String str : permsList){
                authorityList.add(new SimpleGrantedAuthority(str));
            }
        }

        return new User(sysUser.getLoginName(), passwordEncoder.encode(sysUser.getPassword()), authorityList);
    }
}

1.4.4 LoginController登录处理

package com.modules.system.controller;

import com.modules.common.web.BaseController;
import com.modules.system.service.SysUserService;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 登录管理
 * @author lc
 */
@Api(tags = "登录管理")
@Slf4j
@CrossOrigin
@Controller
public class LoginController extends BaseController
{

    @Autowired
    private SysUserService userService;

    /**
     * 自定义登录页面
     * @return
     */
    @GetMapping("/login")
    public String login() {
        return "login";
    }

    /**
     * 登录成功后显示的首页
     * @return
     */
    @GetMapping("/")
    public String index() {
        return "index";
    }

    @RequestMapping("oauth/exit")
    public void exit(HttpServletRequest request, HttpServletResponse response) {
        new SecurityContextLogoutHandler().logout(request, null, null);
        try {
            System.out.println(request.getHeader("referer"));
            response.sendRedirect(request.getHeader("referer"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

1.4.5 自定义登录页面

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>Ela Admin - HTML5 Admin Template</title>
    <meta name="description" content="Ela Admin - HTML5 Admin Template">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <link type="text/css" rel="stylesheet" th:href="@{/assets/css/normalize.css}">
    <link type="text/css" rel="stylesheet" th:href="@{/assets/bootstrap-4.3.1-dist/css/bootstrap.min.css}">
    <link type="text/css" rel="stylesheet" th:href="@{/assets/css/font-awesome.min.css}">
    <link type="text/css" rel="stylesheet" th:href="@{/assets/css/style.css}">

</head>
<body class="bg-dark">
<script type="text/javascript" th:src="@{/assets/js/jquery-2.1.4.min.js}"></script>
<script type="text/javascript" th:src="@{/assets/bootstrap-4.3.1-dist/js/bootstrap.min.js}"></script>
<script type="text/javascript" th:src="@{/assets/js/main.js}"></script>
<div class="sufee-login d-flex align-content-center flex-wrap">
    <div class="container">
        <div class="login-content">
            <div class="login-logo">
                <h1 style="color: #57bf95;">统一登录综合管理平台</h1>
            </div>
            <div class="login-form">
                <form th:action="@{/login}" method="post">
                    <div class="form-group">
                        <label>登录名称</label>
                        <input type="text" class="form-control" name="username" placeholder="请输入登录名称">
                    </div>
                    <div class="form-group">
                        <label>密码</label>
                        <input type="password" class="form-control" name="password" placeholder="请输入登录密码">
                        <span style="color: red" ></span>
                    </div>
                    <div class="checkbox">
                        <label>
                            <input type="checkbox"> 记住我
                        </label>
                        <label class="pull-right">
                            <a href="#">忘记密码</a>
                        </label>
                    </div>
                    <button type="submit" class="btn btn-success btn-flat m-b-30 m-t-30" style="font-size: 18px;">登录</button>
                    <p style="color: red" th:if="${param.error}" th:text="${session?.SPRING_SECURITY_LAST_EXCEPTION?.message}" ></p>
                </form>
            </div>
        </div>
    </div>
</div>
</body>
</html>

1.4.6 登录成功后首页

<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <title>综合管理平台</title>
  <link rel="stylesheet" href="css/style.css">
</head>
<body>
  <div class="container-scroller">
    <!-- partial -->
    <div class="container-fluid page-body-wrapper">
      <div class="main-panel">
        <div class="content-wrapper">
          <div class="page-header">
            <h3 class="page-title">
              <span class="page-title-icon bg-gradient-primary text-white mr-2">
                <i class="mdi mdi-home"></i>                 
              </span>
              欢迎来到综合管理平台
            </h3>
          </div>
          <div class="row">
            <div class="col-md-4 stretch-card grid-margin">
                <div class="card bg-gradient-danger card-img-holder text-white">
                  <div class="card-body">
                    <img src="images/dashboard/circle.svg" class="card-img-absolute" alt="circle-image"/>
                    <h2 class="mb-5"><a href="http://localhost:8086/client1/list" style="color: white">商品管理系统</a></h2>
                  </div>
                </div>
            </div>
            <div class="col-md-4 stretch-card grid-margin">
              <div class="card bg-gradient-info card-img-holder text-white">
                <div class="card-body">
                  <img src="images/dashboard/circle.svg" class="card-img-absolute" alt="circle-image"/>
                  <h2 class="mb-5"><a href="http://localhost:8083/orderSystem/order/list" style="color: white">订单管理系统</a></h2>
                </div>
              </div>
            </div>
            <div class="col-md-4 stretch-card grid-margin">
              <div class="card bg-gradient-success card-img-holder text-white">
                <div class="card-body">
                  <img src="images/dashboard/circle.svg" class="card-img-absolute" alt="circle-image"/>
                  <h2 class="mb-5">营销管理系统</h2>
                </div>
              </div>
            </div>
          </div>

          <div class="row">
            <div class="col-md-4 stretch-card grid-margin">
              <div class="card bg-gradient-danger card-img-holder text-white">
                <div class="card-body">
                  <img src="images/dashboard/circle.svg" class="card-img-absolute" alt="circle-image"/>    
                  <h2 class="mb-5">运营管理系统</h2>
                </div>
              </div>
            </div>
            <div class="col-md-4 stretch-card grid-margin">
              <div class="card bg-gradient-info card-img-holder text-white">
                <div class="card-body">
                  <img src="images/dashboard/circle.svg" class="card-img-absolute" alt="circle-image"/>
                  <h2 class="mb-5">商户管理系统</h2>
                </div>
              </div>
            </div>
            <div class="col-md-4 stretch-card grid-margin">
              <div class="card bg-gradient-success card-img-holder text-white">
                <div class="card-body">
                  <img src="images/dashboard/circle.svg" class="card-img-absolute" alt="circle-image"/>
                  <h2 class="mb-5">财务管理系统</h2>
                </div>
              </div>
            </div>
          </div>
        </div>
        <!-- content-wrapper ends -->
        <!-- partial -->
      </div>
      <!-- main-panel ends -->
    </div>
    <!-- page-body-wrapper ends -->
  </div>
  <!-- container-scroller -->
</body>
</html>

1.4.7 测试

登录页面:admin/123

登录成功

至此,授权服务实现完成,此间使用授权码模式实现,客户端信息存入数据库,登录页面进行自定义。

二 客户端服务

创建Spring Boot2项目,版本:2.3.0,项目名称:cloud-goods-client

2.1 pom.xml

        <!-- oauth2 -->
		<dependency>
			<groupId>org.springframework.security.oauth.boot</groupId>
			<artifactId>spring-security-oauth2-autoconfigure</artifactId>
		</dependency>

		<!-- thymeleaf 模板引擎 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>

		<!-- spring security -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>

2.2 application.yml

server:
  port: 8086
  servlet:
    context-path: /client1

auth-server: http://localhost:8087/upms

security:
  oauth2:
    client:
      client-id: client1
      client-secret: 123456
      user-authorization-uri: ${auth-server}/oauth/authorize
      access-token-uri: ${auth-server}/oauth/token
    resource:
      jwt:
        key-uri: ${auth-server}/oauth/token_key

# thymeleaf 模板引擎配置
spring:
  thymeleaf:
    mode: HTML5
    encoding: utf-8
    servlet:
      content-type: text/html
    # 开发时关闭缓存,不然没法看到实时页面
    cache: true
    # 页面的存放路径就使用默认配置了
    prefix: classpath:/templates/
    check-template-location: true
    suffix: .html

2.3 代码实现

2.3.1 WebSecurityConfigurer配置

package com.modules.config;


import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**").authorizeRequests()
                .anyRequest().authenticated();
    }
}

2.3.2 TestController 测试

package com.modules.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class TestController {

    @GetMapping("/list")
    @PreAuthorize("hasAuthority('ROLE_NORMAL')")
    public String list( ) {
        return "list";
    }
}

2.3.3 自定义模拟列表显示

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>商品管理系统</title>
</head>
<body>
<h3><span>商品管理系统</span> | <a th:href="@{/logout}">退出</a> </h3>
<h2>这是一个商品管理列表页面</h2>
</body>
</html>

2.3.4 测试

访问客户端 http://localhost:8086/client1/list 自动跳转授权服务器登录页面 http://localhost:8087/upms/login

输入登录名称和密码,自动跳转到客户端 http://localhost:8086/client1/list

访问服务器 http://localhost:8087/upms/

因为我在商品管理系统添加了超链接 http://localhost:8086/client1/list,点击商品管理系统,自动跳转到客户端

至此,客户端微服务集成单点登录完成,多个客户端服务如此集成即可。

参考:https://www.cnblogs.com/cjsblog/p/10548022.html

猜你喜欢

转载自blog.csdn.net/lovelichao12/article/details/115195806