admin管理员组

文章数量:1568354

文章目录

    • 单点登陆系统简介
    • 单点登陆系统概述
    • 单点登陆系统解决方案设计
    • 单点登陆系统初步设计
      • 服务设计
      • 服务流程
      • 项目结构设计
    • sso-system基础服务工程实现
      • 业务描述
      • 数据库表结构设计
      • 一、pom文件依赖
      • 二、bootstrap.yml文件(系统优先扫描bootstrap文件名字)
      • 三、添加主启动类
      • 四、添加POJO对象,封装数据库用户信息
      • 五、Controller控制层实现
      • 六、Service业务层实现
      • 七、Mapper持久层实现
    • sso-auth统一认证服务的实现
      • 项目业务描述
      • 一、pom文件依赖
      • 二、bootstrap.yml文件
      • 三、主启动类
      • 四、定义POJO对象,封装数据库的信息
      • 五、Service业务层实现
      • 六、定义用户登录业务逻辑处理对象
      • 七、定义Security配置类
        • Security 认证流程分析
      • 八、测试
    • sso-resource资源服务工程实现
      • 业务描述
      • 业务架构流程图
      • 一、pom文件依赖
      • 二、创建bootstrap.yml文件
      • 三、主启动类
      • 四、封装数据库POJO对象
      • 五、添加自定义注解annotation
      • 六、Controller控制层实现
      • 七、Server层业务实现
      • 八、配置令牌解析器对象
      • 九、配置资源认证授权规则
      • 十、切面业务
    • sso-gateway网关服务实现
      • 业务描述
      • 一、pom文件依赖
      • 二、bootstrap.yml配置文件
      • 三、启动类
      • 四、测试
    • sso-ui客户端UI实现
      • 业务描述
      • 一、pom文件依赖
      • 二、启动类
      • 三、创建UI工程登录界面
    • 项目技术分析
    • 总结
      • 难点分析
      • 问题分析
      • BUG分析

单点登陆系统简介

背景分析
传统的登录系统中,每个站点都实现了自己的专用登录模块。各站点的登录状态相互不认可,各站点需要逐一手工登录。例如:

这样的系统,我们又称之为多点登陆系统。应用起来相对繁琐(每次访问资源服务都需要重新登陆认证和授权)。与此同时,系统代码的重复也比较高。由此单点登陆系统诞生。

单点登陆系统概述

单点登录,英文是 Single Sign On(缩写为 SSO)。即多个站点共用一台认证授权服务器,用户在其中任何一个站点登录后,可以免登录访问其他所有站点。而且,各站点间可以通过该登录状态直接交互。例如:

单点登陆系统解决方案设计

解决方案1:用户登陆成功以后,将用户登陆状态存储到redis数据库。
说明,在这套方案中,用户登录成功后,会基于UUID生成一个token,然后与用户信息绑定在一起存储到数据库(redis缓存).后续用户在访问资源时,基于token从数据库查询用户状态,这种方式因为要基于数据库存储和查询用户状态,所以性能表现一般.

解决方案2:用户登陆成功以后,将用户信息存储到token(令牌),然后写到客户端进行存储。
说明,在这套方案中,用户登录成功后,会基于JWT技术生成一个token,用户信息可以存储到这个token中.后续用户在访问资源时,对token内容解析,检查登录状态以及权限信息,无须再访问数据库.

单点登陆系统初步设计

服务设计

基于单点登陆系统中的业务描述,进行初步服务架构设计,如图所示:

其中,服务基于业务进行划分,系统(system)服务只提供基础数据(例如用户信息,日志信息等),认证服务(auth)负责完成用户身份的校验,密码的比对,资源服务(resource)代表一些业务服务(例如我的订单,我的收藏等等).

服务流程

第一次:客户端UI服务发送request请求→Gateway网关服务进行统一接收请求,然后转发请求→auth服务进行认证,对用户身份进行校验,账户密码是否正确→system服务和数据交互,但不并进行对token进行解析→auth服务认证完毕后通过jwt技术生成一个token然后进行返回→Gateway网关服务接收token然后返回→客户端UI接收token参数
第二次:客户端UI再次发送request请求,这次请求将携带token参数→Gateway网关服务转发请求→resource资源服务接收请求,查看请求是否拥有token令牌,有则接收这个请求,并进行业务的处理,没有这个请求将直接返回并先走上面的一步,进行认证判断→Gateway网关服务→客户端UI

项目结构设计

sso-system基础服务工程实现

业务描述

sso-system服务主要提供基础数据服务,和数据库进行交互,例如日志信息,用户信息等等

数据库表结构设计

一、pom文件依赖

        <!--1.数据库访问相关-->
        <!--1.1 mysql 数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--1.2 mybatis plus 插件-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>
        <!--服务治理相关-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--Web 服务相关-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

二、bootstrap.yml文件(系统优先扫描bootstrap文件名字)

server:
  port: 8061 #端口号
spring:
  application: #微服务工程名
    name: sso-system
  cloud:
    nacos:   #注册中心
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yml
  datasource:  #数据库JDBC
    url: jdbc:mysql://localhost:3305/jt-sso?serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: root

三、添加主启动类

package com.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SystemApplication {
    public static void main(String[] args) {
        SpringApplication.run(SystemApplication.class,args);
    }
}

四、添加POJO对象,封装数据库用户信息

package com.jt.system.pojo;
import lombok.Data;
import java.io.Serializable;

/**
 * 通过此对象封装用户信息
 */
@Data //自动生成get set方法
public class User implements Serializable {
    private static final long serialVersionUID = 4831304712151465443L;
    private Long id;
    private String username;
    private String password;
    private String status;
}

五、Controller控制层实现

实现基于用户名查询用户信息,基于用户id查询用户权限信息的方法

package com.jt.system.controller;

import com.jt.system.pojo.User;
import com.jt.system.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
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.RestController;

import java.util.List;

@RestController
@RequestMapping("/user/")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/login/{username}")
    public User doSelectUserByUsername(
            @PathVariable("username") String username){
        return userService.selectUserByUsername(username);
    }
    @GetMapping("/permission/{userId}")
    public List<String> doSelectUserPermissions(
            @PathVariable("userId") Long userId){
        return userService.selectUserPermissions(userId);
    }
}

六、Service业务层实现

service接口,基于用户名查询用户信息,基于用户id查询用户权限信息的方法

package com.jt.system.service;

import com.jt.system.pojo.User;

import java.util.List;

public interface UserService {
    User selectUserByUsername(String username);
    List<String> selectUserPermissions(Long userId);
}

service接口实现类,基于用户名查询用户信息,基于用户id查询用户权限信息的方法

package com.jt.system.service.impl;

import com.jt.system.dao.UserMapper;
import com.jt.system.pojo.User;
import com.jt.system.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public User selectUserByUsername(String username) {
        return userMapper.selectUserByUsername(username);
    }
    @Override
    public List<String> selectUserPermissions(Long userId) {
        return userMapper.selectUserPermissions(userId);
    }
}

七、Mapper持久层实现

基于用户名查询用户信息,基于用户id查询用户权限信息的方法

package com.jt.system.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.system.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;

@Mapper
public interface UserMapper extends BaseMapper<User> {
    /**
     * 基于用户名获取用户信息
     * @param username
     * @return
     */
    @Select("select id,username,password,status " +
            "from tb_users " +
            "where username=#{username}")
    User selectUserByUsername(String username);

    /**
     * 基于用户id查询用户权限
     * @param userId 用户id
     * @return 用户的权限
     * 涉及到的表:tb_user_roles,tb_role_menus,tb_menus
     */
    @Select("select distinct m.permission " +
            "from tb_user_roles ur join tb_role_menus rm on ur.role_id=rm.role_id" +
            "     join tb_menus m on rm.menu_id=m.id " +
            "where ur.user_id=#{userId}")
    List<String> selectUserPermissions(Long userId);
}

SQL的实现
基于用户名获取用户信息

select id,username,password,status from tb_users where username=admin

基于用户id查询用户权限
方案一:单表多次查询
基于用户id查询用户角色

select role_id from tb_user_roles where user_id=1;

基于用户角色查询菜单id

select menu_id from tb_role_menus where  role_id in (1);

基于菜单id查询菜单的权限标识

select permission from tb_menus where id in (1,2,3);

方案二:多表嵌套查询,先根据用户id查询出角色id,在根据角色id查询出菜单id,最后根据菜单id查询出用户的权限

select permission from tb_menus where id in
		(select menu_id from tb_role_menus where role_id in
				(select role_id from tb_user_roles where user_id=1)
		);

方案三:多表关联查询
在表中,可能会包含重复值。这并不成问题,不过,有时您也许希望仅仅列出不同(distinct)的值。
关键词 distinct用于返回唯一不同的值。

select distinct permission
from tb_menus m join tb_role_menus rm on m.id = rm.menu_id
                join tb_user_roles ur on rm.role_id = ur.role_id
where user_id=1;

sso-auth统一认证服务的实现

项目业务描述

用户在登录时调用次服务队用户的账户和密码进行统一的身份认证和授权token的发放

一、pom文件依赖

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--SSO技术方案:SpringSecurity+JWT+oauth2-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <!--open feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

    </dependencies>

二、bootstrap.yml文件

server:
  port: 8071
spring:
  application:
    name: sso-auth
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848

三、主启动类

package com.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@EnableFeignClients
@SpringBootApplication
public class AuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
    }
}

启动项目,系统会自动生成一个登录密码,在浏览器中进行访问 http://localhost:8071,用户名user,密码生成的面貌

四、定义POJO对象,封装数据库的信息

package com.jt.auth.pojo;

import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
    private static final long serialVersionUID = 4831304712151465443L;
    private Long id;
    private String username;
    private String password;
    private String status;
}

五、Service业务层实现

远程service对象,用于实现用户信息的调用(明修栈道,暗度陈仓)
通过@FeignClient注解来远程调用后sso-system服务,value属性为要调用的服务的名字,contexId为当前fegin接口的名称,首字母小写

package com.jt.auth.service;

import com.jt.auth.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.List;

@FeignClient(value = "sso-system", contextId ="remoteUserService" )
public interface RemoteUserService {

    @GetMapping("/user/login/{username}")
    User selectUserByUsername( @PathVariable("username") String username);

    @GetMapping("/user/permission/{userId}")
    List<String> selectUserPermissions(@PathVariable("userId") Long userId);
}

六、定义用户登录业务逻辑处理对象

UserDetailsServiceImpl接口的实现类负责:

  • 1.负责通过查询内存获取用户信息
  • 2.可以在这里通过dao对象访问数据库获取用户信息
  • 3.可以在这里通过Feign接口调用远端Service获取用户信息(本次选择)
package com.jt.auth.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private RemoteUserService remoteUserService;
    /**
     * 基于用户名获取数据库中的用户信息
     * @param username 这个username来自客户端
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        //基于feign方式获取远程数据并封装
        //1.基于用户名获取用户信息
        com.jt.auth.pojo.User user=
        remoteUserService.selectUserByUsername(username);
        if(user==null)
            throw new UsernameNotFoundException("用户不存在");
        //2.基于用于id查询用户权限
        List<String> permissions=
        remoteUserService.selectUserPermissions(user.getId());
        log.info("permissions {}",permissions);
        //3.对查询结果进行封装并返回
        User userInfo= new User(username,
                user.getPassword(),
                AuthorityUtils.createAuthorityList(permissions.toArray(new String[]{})));
        //......
        return userInfo;
        //返回给认证中心,认证中心会基于用户输入的密码以及数据库的密码做一个比对
    }
}

七、定义Security配置类

Security 认证流程分析

目前的登陆操作,就是用户的认证操作,其实现主要基于Spring Security框架,其认证简易流程如下:

SecurityConfig配置类,主要负责配置认证规则

package com.jt.auth.config;

import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * 当我们在执行登录操作时,底层逻辑(了解):
 * 1)Filter(过滤器)
 * 2)AuthenticationManager (认证管理器)
 * 3)AuthenticationProvider(认证服务处理器)
 * 4)UserDetailsService(负责用户信息的获取及封装)
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

     /**
     * 使用自定义的账号和密码登录时,需要指定密码的加密算法.
     * 当前这个对象使用的Bcrypt加密算法是一个不可逆的加密算法.
     * 类似MD5,但要比MD5更安全.
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    /**
     * 定义认证管理器对象,这个对象负责完成用户信息的认证,
     * 即判定用户身份信息的合法性,在基于oauth2协议完成认
     * 证时,需要此对象,所以这里讲此对象拿出来交给spring管理
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean()
            throws Exception {
      return super.authenticationManagerBean();
    }
}

构建令牌生成及配置对象
借助JWT(Json Web Token-是一种json格式)方式将用户相关信息进行组织和加密,并作为响应令牌(Token),从服务端响应到客户端,客户端接收到这个JWT令牌之后,将其保存在客户端(例如localStorage),然后携带令牌访问资源服务器,资源服务器获取并解析令牌的合法性,基于解析结果判定是否允许用户访问资源.

package com.jt.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;


/**
 * 在此配置类中配置令牌的生成,存储策略,验签方式(令牌合法性)。
 */
@Configuration
public class TokenConfig {

    /**
     * 配置令牌的存储策略,对于oauth2规范中提供了这样的几种策略
     * 1)JdbcTokenStore(这里是要将token存储到关系型数据库)
     * 2)RedisTokenStore(这是要将token存储到redis数据库-key/value)
     * 3)JwtTokenStore(这里是将产生的token信息存储客户端,并且token
     * 中可以以自包含的形式存储一些用户信息)
     * 4)....
     */
    @Bean
    public TokenStore tokenStore(){
        //这里采用JWT方式生成和存储令牌信息
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    /**
     * 配置令牌的创建及验签方式
     * 基于此对象创建的令牌信息会封装到OAuth2AccessToken类型的对象中
     * 然后再存储到TokenStore对象,外界需要时,会从tokenStore进行获取。
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter=
                new JwtAccessTokenConverter();
        //JWT令牌构成:header(签名算法,令牌类型),payload(数据部分),Signing(签名)
        //这里的签名可以简单理解为加密,加密时会使用header中算法以及我们自己提供的密钥,
        //这里加密的目的是为了防止令牌被篡改。(这里密钥要保管好,要存储在服务端)
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);//设置密钥
        return jwtAccessTokenConverter;
    }

    /**
     * JWT 令牌签名时使用的密钥(可以理解为盐值加密中的盐)
     * 1)生成的令牌需要这个密钥进行签名
     * 2)获取的令牌需要使用这个密钥进行验签(校验令牌合法性,是否被篡改过)
     */
    private static final String SIGNING_KEY="auth";
}

定义Oauth2认证授权配置
所有零件准备好了开始拼装最后的主体部分,这个主体部分就是授权服务器的核心配置.

package com.jt.auth.config;

import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
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;

/**
 * Oauth2 是什么? 一种协议或者规范
 * 解决什么问题? 此规范中定义了完成用户身份认证和授权的一种方式
 * 例如: 基于用户名和密码进行认证,基于第三方(vx,qq)应用进行认证...
 *
 * Oauth2规范中定义哪些核心对象?
 *  1.资源对象 (数据库或文件中的数据)
 *  2.资源服务器对象(有你要访问的资源)
 *  3.认证管理器对象(负责用户身份的认证,完成认证后可访问资源)
 *  4.认证客户端对象(password,三方的)
 *  5.用户对象(需要认证的对象)
 *
 * Oauth2规范中如何对用户身份进行认证?
 *   1.提供负责认证的地址?(用户去哪里认证,结婚去民政局...)
 *   2.告诉用户携带什么资料去认证?(携带的信息,例如身份证,户口本...)
 *   3.提供完成用户身份认证的对象?(AuthenticationManager)
 *   4.认证成功后颁发什么令牌?(token)
 *
 * 接下来的配置中就要完成如上几个内容...
 */
@AllArgsConstructor //全参构造注入 省去@Autowired
@EnableAuthorizationServer //表示启动授权服务
@Configuration
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {

    //@Autowired
    private PasswordEncoder passwordEncoder;
    //@Autowired
    private AuthenticationManager authenticationManager;
    //@Autowired
    private TokenStore tokenStore;
    //@Autowired
    private JwtAccessTokenConverter  jwtAccessTokenConverter;

    /**
     * 公开认证的地址?(客户端将认证信息提交到哪里)
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //super.configure(security);
        security.tokenKeyAccess("permitAll()") //公开认证地址(所有请求无需认证就可以访问这个地址)
                .checkTokenAccess("permitAll()") //公开检查令牌的值(所有请求无需认证就可以访问这个地址)
                .allowFormAuthenticationForClients(); //允许form表单认证
    }

    /**
     * 定义客户端认证时,需要携带的信息
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //super.configure(clients);
        clients.inMemory()
                .withClient("gateway-client")//定义客户端标识
                .secret(passwordEncoder.encode("123456"))//定义客户端需要携带的密钥
                .authorizedGrantTypes("password","refresh_token")//定义允许客户端认证的方式(基于密码方式和刷新令牌)
                .scopes("all");//指定作用域(满足如上所有条件的客户端都可以进行认证)
    }

    /**
     * 定义完成用户身份认证的对象
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //super.configure(endpoints);
        //设置认证管理器(这个对象右spring security提供)
        endpoints.authenticationManager(authenticationManager);
        //设置令牌生成机制以及令牌特性
        endpoints.tokenServices(tokenServices());
        //定义允许客户端的请求方式
        //endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
    }

    @Bean
    public AuthorizationServerTokenServices tokenServices(){
        //创建AuthorizationServerTokenServices类型的对象
        DefaultTokenServices tokenServices = new DefaultTokenServices();//token业务对象

        //设置令牌存储方式
        tokenServices.setTokenStore(tokenStore);//令牌机制

        //设置令牌生成方式(令牌增强-默认是UUID)
        tokenServices.setTokenEnhancer(jwtAccessTokenConverter);//令牌增强

        //设置访问令牌的有效时长
        tokenServices.setAccessTokenValiditySeconds(3600);//令牌有效时长

        //设置刷新令牌以及有效时长
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setRefreshTokenValiditySeconds(3600*24);
        return tokenServices;
    }
}

八、测试

### 通过刷新令牌执行登录认证,再请求新的访问令牌
POST http://localhost:8071/oauth/token
Content-Type: application/x-www-form-urlencoded

client_id=gateway-client&client_secret=123456&grant_type=refresh_token&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiIzYjlkMmViYS02N2FhLTQ0MWEtOTE4NS04ZmM3Zjc1ZDUxMzkiLCJleHAiOjE2NDY3OTM3MTEsImF1dGhvcml0aWVzIjpbInN5czpyZXM6Y3JlYXRlIiwic3lzOnJlczpsaXN0Iiwic3lzOnJlczpkZWxldGUiXSwianRpIjoiMWFkNDQ5YWQtNWQ0Ny00MWMyLWIwNjMtMTM4Mzk3Mzg5NDZmIiwiY2xpZW50X2lkIjoiZ2F0ZXdheS1jbGllbnQifQ.aRXmxH9KPQuxC8nZl6cfIGjHoMaWre25McHs_XLkzr4

### 检查token (携带令牌到服务端进行校验)
POST http://localhost:8071/oauth/check_token
Content-Type: application/x-www-form-urlencoded

token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDY3MTA2MDIsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsic3lzOnJlczpjcmVhdGUiLCJzeXM6cmVzOmxpc3QiLCJzeXM6cmVzOmRlbGV0ZSJdLCJqdGkiOiI3MzA0NWE4Zi03MGVkLTQwYjItOTRjYS0yYTEyNGYzMzhkOWMiLCJjbGllbnRfaWQiOiJnYXRld2F5LWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.Oqp1V2DNDcixIf1TxGFpLm0-GVis7tyTuId4qYMWrpk

### 通过账号密码进行登录认证(认证成功,服务端会返回给客户端访问令牌,...)
POST http://localhost:8071/oauth/token
Content-Type: application/x-www-form-urlencoded

username=admin&password=123456&client_id=gateway-client&client_secret=123456&grant_type=password

sso-resource资源服务工程实现

业务描述

当资源服务工程为一个业务数据工程,在访问工程中的资源时候,通过需要考虑那些资源可以匿名访问,不需要登录就可以访问,例如淘宝查看商品,那些资源必须认证了才可以访问,哪些资源必须认证过后,才有权限访问

业务架构流程图

一、pom文件依赖

   <dependencies>
        <!--spring boot web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--nacos discovery-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--nacos config-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--sentinel-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

        <!--在资源服务器添加此依赖,只做授权,不做认证,添加完此依赖以后,
        在项目中我们要做哪些事情?对受限访问的资源可以先判断是否登录了,
        已经认证用户还要判断是否有权限?
        -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
    </dependencies>

二、创建bootstrap.yml文件

server:
  port: 8881
spring:
  application:
    name: sso-resource
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yml

三、主启动类

package com.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ResourceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ResourceApplication.class,args);
    }
}

四、封装数据库POJO对象

封装数据字段

package com.jt.resource.pojo;

import lombok.Data;
import java.io.Serializable;
import java.util.Date;

/**
 * 基于此对象封装用户行为日志?
 * 谁在什么时间执行了什么操作,访问了什么方法,传递了什么参数,访问时长是多少.
 */
@Data
public class Log implements Serializable {

    private static final long serialVersionUID = 913659751396800191L;
    private Long id;
    /**登录用户*/
    private String username;
    /**客户端的ip地址*/
    private String ip;
    /**执行操作的时间*/
    private Date createdTime;
    /**执行的操作名*/
    private String operation;
    /**方法的方法:这里要写类全名.方法名*/
    private String method;
    /**执行方法时传递的实际参数是什么*/
    private String params;
    /**执行这个业务的耗时*/
    private Long time;
    /**业务执行过程是否成功了*/
    private Integer status;
    /**可能出现的错误信息*/
    private String error;
}

五、添加自定义注解annotation

为了获取Controller层访问资源时候是否有权限访问

package com.jt.resource.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {
    String value() default "";
}

六、Controller控制层实现

实现查询资源、创建自语言、修改资源、删除资源,还可以对业务进行拓展,对请求进行鉴权,查看用户是否有执行这个方法的权限

package com.jt.resource.controller;

import com.jt.resource.annotation.RequiredLog;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/resource")
public class ResourceController {

    /**
     * @PreAuthorize 注解描述的方法为一个权限切入点方法,这个方法执行之前首先要进行鉴权,就是判定用户是否有执行
     *                 这个方法的权限,这里的"sys:res:list"表示,访问此方法需要的权限
     */
    @RequiredLog(value = "查询资源")
    @GetMapping
    @PreAuthorize("hasAuthority('sys:res:list')") //鉴权 切入点方法
    public String doSelect(){
        return "select resource";
    }

    @RequiredLog(value="修改资源")
    @PutMapping
    @PreAuthorize("hasAuthority('sys:res:update')")
    public String doUpdate(){
        return "update resource";
    }

    @DeleteMapping
    @PreAuthorize("hasAuthority('sys:res:delete')")
    public String doDelete(){
        return "delete resource";
    }

    @RequiredLog(value="创建资源")
    @PostMapping
    @PreAuthorize("hasAuthority('sys:res:create')")
    public String doCreate(){
        return "Create resource";
    }
}

七、Server层业务实现

package com.jt.resource.service;

import com.jt.resource.pojo.Log;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * 日志远程服务调用接口,在这里我们要调用system服务去将用户行为日志写入到数据库中
 */
@FeignClient(value="sso-system",contextId = "remoteLogService")
public interface RemoteLogService {
    @PostMapping("/log")
    void insertLog(@RequestBody Log userLog);
}

八、配置令牌解析器对象

package com.jt.resource.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;

//需要什么传什么(Oauth2Config需要的)
@Configuration
/**
 * 定义用户状态的存储方式
 *  1.JdbcTokenStore
 *  2.RedisTokenStore
 *  3.JwtTokenStore
 *  4.InMenoryTokenStore
 */
public class TokenConfig {
    //提出来的配置类
    private static final String SIGNING_KEY="auth";

    //定义令牌转换器对象,底层会基于此对象
    //  1.将json数据构建为Jwt令牌
    //  2.将Jwt令牌转换为json
    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        //构建JWT令牌转换器,负责将json转换为JWT令牌,也可以将jwt令牌转为json
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        //设置JWT令牌的签名key
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }

}

九、配置资源认证授权规则

思考?对于一个系统而言,它资源的访问权限你是如何进行分类设计的

  • 1)不需要登录就可以访问(例如12306查票)
  • 2)登录以后才能访问(例如12306的购票)
  • 3)登录以后没有权限也不能访问(例如会员等级不够不让执行一些相关操作)
package com.jt.resource.config;

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.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
/**
 * 在当前资源中将资源分为3类 —— _ —— !
 *   1.无需认证(登录)就可以访问的资源—— ——匿名资源
 *   2.必须认证才可以访问的资源—— ——类似我的订单
 *   3.认证后必须有访问资源的权限才可以访问的资源—— ——类似访问别人的订单
 *
 * 其中:
 *  1.@EnableResourceServerr表示启动资源服务配置
 *  2.@EnableGlobalMethodSecurity表示启动方法层面的权限控制(思考访问资源是否通过方法,当不允许对这个方法进行访问,
 *     就意味着不允许访问资源),prePostEnabled表示在执行方法之前要检查是否有执行方法的权限
 */
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableResourceServer
@Configuration
public class ResourceConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //super.configure(http);
        //1.禁用跨域攻击(假如不禁用,我们使用postman类似的工具发请求时,可能会出现403异常)
        http.csrf().disable();
        //2.配置资源访问规则
        http.authorizeRequests()
                //以"/resource/**"开头的所有url必须认证
                .antMatchers("/resource/**")
                .authenticated()
                //除了上面需要认证的,其它则可以匿名访问
                .anyRequest()
                .permitAll();
    }
}

十、切面业务

主要是接收拦截controller层方法的注解是,实现业务的拓展

package com.jt.resource.aspect;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jt.resource.annotation.RequiredLog;
import com.jt.resource.pojo.Log;
import com.jt.resource.service.RemoteLogService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;

@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
@Component
@Aspect
public class LogAspect {

    @Autowired
    private RemoteLogService remoteLogService;

    @Pointcut("@annotation(com.jt.resource.annotation.RequiredLog)")
    public void doLog(){
        //这个方法主要是为了承载切入点的定义
    }
    @Around("doLog()")
    public Object doAroud(ProceedingJoinPoint joinPoint) throws Throwable{
        int status = 1;
        long time =-0;
        String error = "";
        //1.获取方法开始执行的时间,并输出
        long t1 = System.currentTimeMillis();
        log.debug("before.time {}",t1);
        try {
            //2.调用目标执行链,这个执行链的终点是你的切入点方法
            //执行结果为切入点方法的执行结果
            Object result = joinPoint.proceed();
            //3.获取方法执行结束的数据,并输出
            long t2 = System.currentTimeMillis();
            log.debug("after.time {}",t2);
            time = t2 - t1;
            return result;
        }catch (Throwable e){
            long t3 = System.currentTimeMillis();
            log.error("exception.time {}",t3);
            time = t3 - t1;
            status = 0;
            error = e.getMessage();
            throw e;
        }finally {
            //进行日志记录
            //这里的保存是将日志最后传递给system工程
            saveUserLog(joinPoint,time,status,error);
        }
    }
    private void saveUserLog(ProceedingJoinPoint joinPoint,Long time,int status,String error)
            throws NoSuchMethodException, JsonProcessingException {
        //1.获取用户行为日志
        //1.1获取ip地址(首先要获取请求对象,查spring中获取请求对象的方式)
        //RequestContextHolder对象是当前线程中请求对象的持有者
        ServletRequestAttributes requestAttributes =
                (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        String ip = requestAttributes.getRequest().getRemoteAddr();

        //1.2获取登录用户名(借助SecurityContextHolder,此对象中存储了用户的认证信息)
        String username = (String)SecurityContextHolder.getContext().getAuthentication().getPrincipal();

        //1.3获取用户操作(日志切入点方法上@RequiredLog注解中value的值)
        //1.3.1获取代理对象对应的目标对象类型
        Class<?> targetCls = joinPoint.getTarget().getClass();

        //1.3.2获取目标类型中指定的方法(方法信息可通过joinPoint获取方法签名信息)
        MethodSignature methodSignature =  //方法签名-存储了方法信息的一个对象
                (MethodSignature) joinPoint.getSignature();
        Method targetMethod =
                targetCls.getDeclaredMethod(methodSignature.getName(), methodSignature.getParameterTypes());

        //1.3.3获取方法上的RequiredLog注解
        RequiredLog requiredLog = targetMethod.getAnnotation(RequiredLog.class);

        //1.3.4获取方法上的RequiredLog注解中的操作名
        String operation=requiredLog.value();

        //1.4获取目标方法名(这里要求类全名+方法名)
        String targetMethodName = String.format("%s.%s",targetCls.getName(),targetMethod.getName());

        //1.5获取请求参数(客户端传递的实际参数)
        // String params=joinPoint.getArgs().toString();//不推荐
        String params = new ObjectMapper().writeValueAsString(joinPoint.getArgs());//推荐将参数转换为json格式字符串

        //2.封装用户行为日志
        Log userLog = new Log();
        userLog.setIp(ip);
        userLog.setUsername(username);
        userLog.setCreatedTime(new java.util.Date());
        userLog.setOperation(operation);
        userLog.setMethod(targetMethodName);
        userLog.setParams(params);
        userLog.setTime(time);
        userLog.setStatus(status);
        userLog.setError(error);
        //3.持久化用户行为日志
        log.debug("user log: {}", userLog);
        remoteLogService.insertLog(userLog);
    }
}

sso-gateway网关服务实现

业务描述

API网关是服务访问入口,身份认证,资源访问都通过网关进行资源统一转发

一、pom文件依赖

 <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--假如网关层面进行限流,添加如下依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
    </dependencies>

二、bootstrap.yml配置文件

server:
  port: 9000
spring:
  application:
    name: sso-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yml
    sentinel:
      transport:
        dashboard: localhost:8180
      eager: true
    gateway:
      routes:
        - id: router01
          uri: lb://sso-resource
          predicates:
            - Path=/sso/resource/**
          filters:
            - StripPrefix=1
        - id: router02
          uri: lb://sso-auth
          predicates:
            - Path=/sso/oauth/**
          filters:
            - StripPrefix=1
      globalcors: #跨域配置(写到配置文件的好处是可以将其配置写到配置中心)
        corsConfigurations:
          '[/**]':
            allowedOrigins: "*"
            allowedHeaders: "*"
            allowedMethods: "*"
            allowCredentials: true

三、启动类

package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ApiGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

四、测试

### 通过账号密码进行登录认证(认证成功,服务端会返回给客户端访问令牌,...)
POST http://localhost:9000/sso/oauth/token
Content-Type: application/x-www-form-urlencoded

username=user&password=123456&client_id=gateway-client&client_secret=123456&grant_type=password

### 查询ResourceController中的资源(需要认证后,有权限才可以访问)
GET http://localhost:9000/sso/resource
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDY3MjU0NjksInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsic3lzOnJlczpjcmVhdGUiLCJzeXM6cmVzOmxpc3QiLCJzeXM6cmVzOmRlbGV0ZSJdLCJqdGkiOiI1YThjMTUxYi1hZmY0LTRkZWEtYjI2ZS03MzRhNmIyODVhY2MiLCJjbGllbnRfaWQiOiJnYXRld2F5LWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.Ac4xtVhGdLWyaNAFwRMDu85iTg2MRj1AqoLZZpoWZzc

### 更新ResourceController中的资源(需要认证后,有权限才可以访问)
PUT http://localhost:9000/sso/resource
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDY3MjU0NjksInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsic3lzOnJlczpjcmVhdGUiLCJzeXM6cmVzOmxpc3QiLCJzeXM6cmVzOmRlbGV0ZSJdLCJqdGkiOiI1YThjMTUxYi1hZmY0LTRkZWEtYjI2ZS03MzRhNmIyODVhY2MiLCJjbGllbnRfaWQiOiJnYXRld2F5LWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.Ac4xtVhGdLWyaNAFwRMDu85iTg2MRj1AqoLZZpoWZzc

### 更新ResourceController中的资源(需要认证后才可以访问)
DELETE http://localhost:9000/sso/resource
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDY3MjU0NjksInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsic3lzOnJlczpjcmVhdGUiLCJzeXM6cmVzOmxpc3QiLCJzeXM6cmVzOmRlbGV0ZSJdLCJqdGkiOiI1YThjMTUxYi1hZmY0LTRkZWEtYjI2ZS03MzRhNmIyODVhY2MiLCJjbGllbnRfaWQiOiJnYXRld2F5LWNsaWVudCIsInNjb3BlIjpbImFsbCJdfQ.Ac4xtVhGdLWyaNAFwRMDu85iTg2MRj1AqoLZZpoWZzc

sso-ui客户端UI实现

业务描述

项目采用前后端分离架构设计,前端工程基于springboot web服务进行实现

一、pom文件依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

二、启动类

package com.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UIApplication {
    public static void main(String[] args) {
        SpringApplication.run(UIApplication.class, args);
    }
}

三、创建UI工程登录界面

创建login.html界面

<!doctype html>
<html lang="en">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>login</title>
</head>
<body>
<div class="container"id="app">
    <h3>Please Login</h3>
    <form>
        <div class="mb-3">
            <label for="usernameId" class="form-label">Username</label>
            <input type="text" v-model="username" class="form-control" id="usernameId" aria-describedby="emailHelp">
        </div>
        <div class="mb-3">
            <label for="passwordId" class="form-label">Password</label>
            <input type="password" v-model="password" class="form-control" id="passwordId">
        </div>
        <button type="button" @click="doLogin()" class="btn btn-primary">Submit</button>
    </form>
</div>
<script src="https://cdn.jsdelivr/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr/npm/vue/dist/vue.js"></script>
<script src="https://unpkg/axios/dist/axios.min.js"></script>
<script>
    var vm=new Vue({
        el:"#app",//定义监控点,vue底层会基于此监控点在内存中构建dom树
        data:{ //此对象中定义页面上要操作的数据
            username:"",
            password:""
        },
        methods: {//此位置定义所有业务事件处理函数
            doLogin() {
                //1.定义url
                let url = "http://localhost:9000/sso/oauth/token"
                //2.定义参数
                let params = new URLSearchParams()
                params.append('username',this.username);
                params.append('password',this.password);
                params.append('client_id',"gateway-client");
                params.append('client_secret',"123456");
                params.append('grant_type',"password");
                //3.发送异步请求
                axios.post(url, params)
                    .then((response) => {//ok
                         alert("login ok")
                         let result=response.data;
                         console.log("result",result);
                         //将返回的访问令牌存储到浏览器本地对象中
                         localStorage.setItem("accessToken",result.access_token);
                         location.href="/resource.html";
                         //启动一个定时器,一个小时以后,向认证中心发送刷新令牌
                     })
                    .catch((e)=>{
                        console.log(e);
                    })
            }
        }
    });
</script>
</body>
</html>

创建资源展现界面resource.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <h1>The Resource Page</h1>
    <button onclick="doSelect()">查询我的资源</button>
    <button onclick="doUpdate()">修改我的资源</button>
</div>
<script src="https://unpkg/axios/dist/axios.min.js"></script>
<script>
    function doSelect(){
        let url="http://localhost:9000/sso/resource";
        //获取登录后,存储到浏览器客户端的访问令牌
        let token=localStorage.getItem("accessToken");
        //发送请求时,携带访问令牌
        axios.get(url,{headers:{"Authorization":"Bearer "+token}})
            .then(function (response){
                alert("select ok")
                console.log(response.data);
            })
            .catch(function (e){//失败时执行catch代码块
                if(e.response.status==401){
                    alert("请先登录");
                    location.href="/login.html";
                }else if(e.response.status==403){
                    alert("您没有权限")
                }
                console.log("error",e);
            })
    }
    function doUpdate(){
        let url="http://localhost:9000/sso/resource";
        //获取登录后,存储到浏览器客户端的访问令牌
        let token=localStorage.getItem("accessToken");
        console.log("token",token);
        //发送请求时,携带访问令牌
        axios.put(url,"",{headers:{"Authorization":"Bearer "+token}})
            .then(function (response){
                alert("update ok")
                console.log(response.data);
            })
            .catch(function (e){//失败时执行catch代码块
                console.log(e);
                if(e.response.status==401){
                    alert("请先登录");
                    location.href="/login.html";
                }else if(e.response.status==403){
                    alert("您没有权限")
                }
                console.log("error",e);
            })
    }
</script>
</body>
</html>

项目技术分析

背景分析
企业中数据是最重要的资源,对于这些数据而言,有些可以直接匿名访问,有些只能登录以后才能访问,还有一些你登录成功以后,权限不够也不能访问.总之这些规则都是保护系统资源不被破坏的一种手段.几乎每个系统中都需要这样的措施对数据(资源)进行保护.我们通常会通过软件技术对这样业务进行具体的设计和实现.早期没有统一的标准,每个系统都有自己独立的设计实现,但是对于这个业务又是一个共性,后续市场上就基于共性做了具体的落地实现,例如Spring Security,Apache shiro,JWT,Oauth2等技术诞生了.

Spring Security 技术
Spring Security 是一个企业级安全框架,由spring官方推出,它对软件系统中的认证,授权,加密等功能进行封装,并在springboot技术推出以后,配置方面做了很大的简化.现在市场上分布式架构中的安全控制,正在逐步的转向Spring Security。Spring Security 在企业中实现认证和授权业务时,底层构建了大量的过滤器,如图所示:
其中:
图中绿色部分为认证过滤器,黄色部分为授权过滤器。Spring Security就是通过这些过滤器然后调用相关对象一起完成认证和授权操作.

Jwt 数据规范
JWT(JSON WEB Token)是一个标准,采用数据自包含方式进行json格式数据设计,实现各方安全的信息传输,其官方网址为:https://jwt.io/。官方JWT规范定义,它构成有三部分,分别为Header(头部),Payload(负载),Signature(签名),其格式如下:
xxxxx.yyyyy.zzzzz
Header部分
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
“alg”: “HS256”,
“typ”: “JWT”
}
上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(简写HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。最后,将这个 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。

Payload部分
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT规范中规定了7个官方字段,供选用(了解)。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{
“sub”: “1234567890”,
“name”: “John Doe”,
“admin”: true
}
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。

Signature部分
Signature 部分是对前两部分的签名,其目的是防止数据被篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + “.” +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

Oauth2规范
oauth2定义了一种认证授权协议,一种规范,此规范中定义了四种类型的角色:
1)资源有者(User)
2)认证授权服务器(jt-auth)
3)资源服务器(jt-resource)
4)客户端应用(jt-ui)
同时,在这种协议中规定了认证授权时的几种模式:
1)密码模式 (基于用户名和密码进行认证)
2)授权码模式(就是我们说的三方认证:QQ,微信,微博,。。。。)

总结

难点分析

单点登陆系统的设计架构(微服务架构)
服务的设计及划分(资源服务器,认证服务器,网关服务器,客户端服务)
认证及资源访问的流程(资源访问时要先认证再访问)
认证和授权时的一些关键技术(Spring Security,Jwt,Oauth2)

问题分析

为什么要单点登陆(分布式系统,再访问不同服务资源时,不要总是要登陆,进而改善用户体验)
单点登陆解决方案?(市场常用两种: spring security+jwt+oauth2,spring securit+redis+oauth2)
Spring Security 是什么?(spring框架中的一个安全默认,实现了认证和授权操作)
JWT是什么?(一种令牌格式,一种令牌规范,通过对JSON数据采用一定的编码,加密进行令牌设计)
OAuth2是什么?(一种认证和授权规范,定义了单点登陆中服务的划分方式,认证的相关类型)

BUG分析

401 : 访问资源时没有认证。
403 : 访问资源时没有权限。
404:访问的资源找不到(一定要检查你访问资源的url)
405: 请求方式不匹配(客户端请求方式是GET,服务端处理请求是Post就是这个问题)
500: 不看后台无法解决?(error,warn)

本文标签: 单点系统优化系统