admin管理员组

文章数量:1568307

1.单点登陆系统概述

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

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

2.1解决方案一:用户登陆成功以后,将用户登陆状态存储到redis数据库


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

2.2解决方案2:用户登陆成功以后,将用户信息存储到token(令牌),然后写到客户端进行存储


在这套方案中,用户登录成功后,会基于JWT技术生成一个token,用户信息可以存储到这个token中.后续用户在访问资源时,对token内容解析,检查登录状态以及权限信息,无须再访问数据库

3.单点登陆系统初步设计

3.1服务设计


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

3.2创建项目工程

父工程:02-sso
配置pom文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache/POM/4.0.0"
         xmlns:xsi="http://www.w3/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache/POM/4.0.0 http://maven.apache/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jt</groupId>
    <artifactId>02-sso</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!--maven父工程的pom文件中一般要定义子模块,
    子工程中所需依赖版本的管理,公共依赖并且父工程的打包方式一般为pom方式-->

    <!--第一步: 定义子工程中核心依赖的版本管理(注意,只是版本管理)-->
    <dependencyManagement>
        <dependencies>
            <!--spring boot 核心依赖版本定义(spring官方定义)-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.3.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!--Spring Cloud 微服务规范(由spring官方定义)-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR9</version>
                <type>pom</type><!--假如scope是import,type必须为pom-->
                <scope>import</scope><!--引入三方依赖的版本设计-->
            </dependency>

            <!--Spring Cloud alibaba 依赖版本管理 (参考官方说明)-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.6.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <!--第二步: 添加子工程的所需要的公共依赖-->
    <dependencies>
        <!--lombok 依赖,子工程中假如需要lombok,不需要再引入-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope><!--provided 表示此依赖仅在编译阶段有效-->
        </dependency>
        <!--单元测试依赖,子工程中需要单元测试时,不需要再次引入此依赖了-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope><!--test表示只能在test目录下使用此依赖-->
            <exclusions>
                <exclusion><!--排除一些不需要的依赖-->
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>junit-jupiter-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--其它依赖...-->
    </dependencies>
    <!--第三步: 定义当前工程模块及子工程的的统一编译和运行版本-->
    <build><!--项目构建配置,我们基于maven完成项目的编译,测试,打包等操作,
    都是基于pom.xml完成这一列的操作,但是编译和打包的配置都是要写到build元素
    内的,而具体的编译和打包配置,又需要plugin去实现,plugin元素不是必须的,maven
    有默认的plugin配置,常用插件可去本地库进行查看-->
        <plugins>
            <!--通过maven-compiler-plugin插件设置项目
            的统一的jdk编译和运行版本-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <!--假如本地库没有这个版本,这里会出现红色字体错误-->
                <version>3.8.1</version>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.3系统基础服务工程设计及实现

本次设计系统服务(System),主要用于提供基础数据服务,例如日志信息,用户信息等

3.3.1数据库表设计

3.3.2创建子工程sso-system

3.3.2.1添加依赖:
<!--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>
3.3.2.2在项目中添加bootstrap.yml文件
server:
  port: 8061
spring:
  application:
    name: sso-system
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yml
  datasource:
    url: jdbc:mysql:///jt-sso?serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: 123456
mybatis-plus:
  type-aliases-package: com.jt.system.pojo
  #将所有的映射文件全部加载
  mapper-locations: classpath:/mappers/*.xml
  #开启驼峰映射
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.jt: debug    

可将连接数据库的配置,添加到配置中心

3.3.2.3在项目中添加启动类
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);
    }
}
3.3.2.4在项目中添加单元测试类,测试数据库连接
package com.jt;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@SpringBootTest
public class DataSourceTests {
    @Autowired
    private DataSource dataSource;//HikariDataSource
    @Test
    void testGetConnection() throws SQLException {
        Connection conn=
        dataSource.getConnection();
        System.out.println(conn);
    }
}

3.4POJO对象逻辑实现

添加项目User对象,用于封装用户信息

java中所有用于存储数据的对象,都建议实现序列化接口,并且添加一个序列化id
可参考:String,Integer,ArrayList,HashMap

@Data //编译时有效
@Accessors(chain = true)
//@TableName("tb_users"),假如sql语句自己写,不需要此注解指定表名
public class User implements Serializable {//此接口起标识性作用
    //序列化id
    private static final long serialVersionUID = -9218088594214708448L;
    private Long id;
    private String username;
    private String password;
    private String status;
}

实现序列化id,alt+enter自动添加UID

Service中@Autowired注入UserMapper中对象红色波浪线解决


Java中连接池设计需要遵循的数据源规范是(javax.sql.DataSource),连接池这块能想到的设计模式有(单例,享元,桥接,slf4j门面)

3.5创建UserMapper接口,并定义基于用户名查询用户信息,基于用户id查询用户权限信息的方法

@Mapper
public interface UserMapper extends BaseMapper<User> {
    @Select("select id,username,password,status from tb_users where username=#{username}")
    User selectUserByUsername(String username);

    @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);
}

3.6创建UserMapperTests类,对业务方法做单元测试

@SpringBootTest
public class UserMapperTests {

    @Autowired
    private UserMapper userMapper;

    @Test
    void testSelectUserByUsername(){
        User user = userMapper.selectUserByUsername("admin");
        System.out.println(user);
        //断言测试,测试结果不正确,就抛异常
        //Assert.notNull(user, "user is not exist");
        //推荐此种方式
        Assertions.assertNotNull(user,"user is not null");
    }
    @Test
    void testSelectUserPermissions(){
        List<String> permissions = userMapper.selectUserPermissions(1L);
        System.out.println(permissions);
        Assertions.assertNotNull(permissions,"permissions is not null");
    }
}

3.7定义service接口

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

3.8定义service接口实现类

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;
    @Override
    public User selectUserByUsername(String username) {
        //Assert.notNull(username,"username can not be null");
        //判断是否为空,抛异常
        if (StringUtils.isEmpty(username)){
            throw new IllegalArgumentException("username can not be null");
        }
        return userMapper.selectUserByUsername(username);
    }

    /**
     * 此注解描述的方法为缓存切入点方法,从数据库查询到数据后,
     * 可以将数据存储到本地的一个缓存对象中(底层是一个map对象)
     * */
    @Cacheable(value = "permissionCache")
    @Override
    public List<String> selectUserPermissions(Long userId) {
        return userMapper.selectUserPermissions(userId);
    }
}

@EnableCaching //开启spring中的缓存机制,扫描哪个类中用到了缓存
@Cacheable(value = "permissionCache"),此注解描述的方法为缓存切入点方法,从数据库查询到数据后, 可以将数据存储到本地的一个缓存对象中(底层是一个map对象)

3.9Controller对象逻辑实现

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

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

4.统一认证工程设计及实现

用户登陆时调用此工程对用户身份进行统一身份认证和授权。

4.1创建工程sso-auth及初始化

4.1.1编辑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>

4.1.2在sso-auth工程中创建bootstrap.yml文件

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

4.1.3添加项目启动类

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

4.1.4启动并访问项目

项目启动时,系统会默认生成一个登陆密码

打开浏览器输入http://localhost:8071呈现登录页面
默认用户名为user,密码为系统启动时,在控制台呈现的密码。执行登陆测试,登陆成功进入如下界面(因为没有定义登录页面,所以会出现404)。

4.2定义用户信息处理对象

第一步:定义User对象,用于封装从数据库查询到的用户信息

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;
}

第二步:定义用户远程调用Feign接口RemoteUserService ,基于此接口调用sso-system服务中的用户信息

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

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

    /**
     * 基于用户id查权限
     * */
    @GetMapping("/user/permission/{userId}")
    List<String> selectUserPermissions(@PathVariable("userId") Long userId);
}

第三步:定义远程Service对象,用于实现远程用户信息调用

/**
 * 构建UserDetailsService实现类,在此类基于RemoteUserService接口进行远程服务调用,
 * 调用sso-system服务,
 */
@Slf4j
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private RemoteUserService remoteUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //1.基于用户名获取用户信息(远程Feign方式服务调用)
        com.jt.auth.pojo.User user = remoteUserService.selectUserByUsername(username);
        if(user == null)
            throw new UsernameNotFoundException("user id not exist");
        //2.基于用户id获取用户权限信息
        List<String> permissions = remoteUserService.selectUserPermissions(user.getId());
        log.debug("permissions {}",permissions);
        //3.封装用户信息并返回
        User userDetails = new User(username,
                user.getPassword(),
                AuthorityUtils.createAuthorityList(permissions.toArray(new String[]{})));
        return userDetails; //交给spring security的认证中心,进行认证分析(比对)
    }
}

5.定义Security配置类

在此类中配置认证规则

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 初始化加密对象
     * 此对象提供了一种不可逆的加密方式,相对于md5方式会更加安全
     */
    @Bean
    public PasswordEncoder passwordEncoder() {

        return new BCryptPasswordEncoder();
    }
    /**配置认证规则
     * 此方法为http请求方法,可配置:
     * 哪些资源放行(不用登录即可访问),不做任何配置,默认所有资源都可匿名访问
     * 哪些资源必须认证(登录)才能访问
     * */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //super.configure(http);
        //禁用跨域攻击,假如没有禁用,使用postman,httpclient这些工具登录失败403
        //http.csrf().disable();
        //所有**资源必须认证才能访问,403没有权限
        //http.authorizeRequests().antMatchers("/**").authenticated();
        //放行所有的资源(在资源服务中授权)
        //http.authorizeRequests().anyRequest().permitAll();
        //配置需要认证的,例如default.html,其它的都放行
/*        http.authorizeRequests()
                .antMatchers("/default.html")
                .authenticated()
                .anyRequest().permitAll();*/
        //登录配置,去哪里认证,认证成功或失败的处理器是谁
//        http.formLogin().defaultSuccessUrl("index.html"); //redirect:index.html重定向
//        http.formLogin().successForwardUrl("/doIndex");

        //前后端分离的写法,登录成功要返回json字符串
        http.formLogin()
                .successHandler(successHandler())
                .failureHandler(failureHandler());
    }

    //定义认证成功处理器
    //登录成功以后返回json数据
    @Bean
    public AuthenticationSuccessHandler successHandler() {
        return new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(
                    HttpServletRequest request,
                    HttpServletResponse response,
                    Authentication authentication) throws IOException, ServletException {
                Map<String,Object> map = new HashMap<>();
                map.put("status", 200);
                map.put("message", "login success");
                writeJsonToClient(response,map);
            }
        };
    }
    //lambda
    public AuthenticationFailureHandler failureHandler() {
        return (request, response, authentication) ->{
                Map<String,Object> map = new HashMap<>();
                map.put("status", 201);
                map.put("message", "login failure");
                writeJsonToClient(response,map);
        };
    }
    private void writeJsonToClient(HttpServletResponse response,Map<String,Object> map) throws IOException {
        //将map对象,转换为json
        String jsonStr =new ObjectMapper().writeValueAsString(map);
        //设置响应数据的编码方式
        response.setCharacterEncoding("utf-8");
        //设置响应数据的类型
        response.setContentType("application/json;charset:utf8");
        //将数据响应到客户端
        PrintWriter out=response.getWriter();
        out.println(jsonStr);
        out.flush();
    }
}

基于浏览器进行访问测试

启动sso-system,sso-auth服务,然后基于浏览器访问网关,执行登录测试,admin,123456

6.Security 认证流程分析

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

6.1定义Oauth2认证授权配置

Oauth2是一种协议或规范,定义了完成用户身份认证和授权的方式,
比如:基于密码身份认证,基于指纹,基于第三方令牌认证(QQ,微信登录),但具体完成过程需要一组对象,这些对象构成,有如下几个部分:
0.系统数据资源(类似数据库,文件系统)
1.资源服务器(负责访问资源,例如:商品,订单,库存,会员…)
2.认证服务器(负责完成用户身份的认证)
3.客户端对象(表单,令牌,)
4.资源拥有者(用户)

在Oauth2这中规范下,如何对用户什么进行认证?
1.认证的地址(让用户去哪里认证)
2.用户需要携带什么信息去认证(办理)
3.具体完成认证的对象是谁

在Security配置类添加以下方法,在Oauth2Config注入此对象:

//方法返回的对象为后续的oauth2的配置提供服务
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

定义Oauth2Config配置类:

/**
 * Oauth2是一种协议或规范,定义了完成用户身份认证和授权的方式,
 * 比如:基于密码身份认证,基于指纹,基于第三方令牌认证(QQ,微信登录),
 * 但具体完成过程需要一组对象,这些对象构成,有如下几个部分:
 * 0.系统数据资源(类似数据库,文件系统)
 * 1.资源服务器(负责访问资源,例如:商品,订单,库存,会员...)
 * 2.认证服务器(负责完成用户身份的认证)
 * 3.客户端对象(表单,令牌,)
 * 4.资源拥有者(用户)
 *
 * 在Oauth2这中规范下,如何对用户什么进行认证?
 * 1.认证的地址(让用户去哪里认证)
 * 2.用户需要携带什么信息去认证(办理)
 * 3.具体完成认证的对象是谁
 */

@Configuration
@EnableAuthorizationServer
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    //提供一个认证的入口(客户端去哪里认证)?(http://ip:port/.....)

    /**
     * 在此方法中公开认证地址
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //super.configure(security);
        //公开认证地址(/auth/token)
        security.tokenKeyAccess("permitAll()")
                //公开检查令牌的入口(/oauth/check_token)
                .checkTokenAccess("permitAll()")
                //允许通过表单方式进行认证
                .allowFormAuthenticationForClients();
    }

    /**
     * 定义用户去认证时,需要携带什么信息
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //super.configure(clients);
        clients.inMemory()
                //客户端标识
                .withClient("gateway-client")
                //客户端携带密钥 123456
                .secret(passwordEncoder.encode("123456"))
                //定义认证类型(允许对哪些数据进行认证)
                .authorizedGrantTypes("password","refresh_token")
                //作用域,满足如上条件的所有客户端可以来这里进行认证
                .scopes("all");


    }

    /**
     * 定义由谁完成认证,认证成功后以怎样形式颁发令牌
     * 默认是UUID方式
     * */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //super.configure(endpoints);
        //定义由谁完成认证
        endpoints.authenticationManager(authenticationManager);
        //定义用户状态信息的存储(存储到内存,mysql,redis,jwt)
        //endpoints.tokenStore(tokenStore());
        //设置令牌增强
        //endpoints.tokenEnhancer(jwtAccessTokenConverter());
        //定义令牌业务存储对象(自己指定令牌规则)
        endpoints.tokenServices(tokenServices());
        //允许客户端认证的请求方式(默认只支持post)
        endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);

    }
    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        //设置令牌存储对象
        tokenServices.setTokenStore(tokenStore);
        //进行令牌增强(普通令牌改位jwt令牌)
        tokenServices.setTokenEnhancer(jwtAccessTokenConverter);
        //设置访问令牌有效期(访问令牌激素hi访问资源时要携带的信息)
        tokenServices.setAccessTokenValiditySeconds(3600);   //
        //设置刷新令牌,在范围跟令牌即将到期时,还可以使用刷新令牌再去请求一个jwt令牌
        tokenServices.setSupportRefreshToken(true);
        //设置刷新令牌有效期
        tokenServices.setRefreshTokenValiditySeconds(7200);
        return  tokenServices;

    }
}

6.2构建令牌生成及配置对象

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

@Configuration
public class TokenConfig {
    /**
     * 创建令牌信息存储对象
     * */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        //设置签名key,对Jwt令牌进行签名时使用,key不能让客户端知道
        jwtAccessTokenConverter.setSigningKey(signingKey);
        return jwtAccessTokenConverter;
    }

    /**签名key,对Jwt令牌签名时使用,这个值可以自己随意定义,*/
    private String signingKey="auth";
}

6.3启动postman进行访问测试


检查token信息:

7.资源服务工程设计及实现

资源服务工程为一个业务数据工程,此工程中数据在访问通常情况下是受限访问,例如有些资源有用户,都可以方法,有些资源必须认证才可访问,有些资源认证后,有权限才可以访问。

7.1业务设计架构

用户访问资源时的认证,授权流程设计如下:

7.2创建sso-resource工程

7.2.1第一步:初始化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>

7.2.2第二步:创建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

7.2.3第三步:创建启动类

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);
    }
}

7.2.4第四步:创建资源Controller对象

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

    /**
     * 查询资源
     * @return
     */
    @PreAuthorize("hasAuthority('sys:res:list')")//描述的是一个切入点方法,访问此方法,需加此权限
    @GetMapping
    public String doSelect(){
        return "Select Resource ok";
    }
    /**
     * 创建资源
     * @return
     */
    @PreAuthorize("hasAuthority('sys:res:create')")
    @PostMapping
    public String doCreate(){
        return "Create Resource OK";
    }
    /**
     * 修改资源
     * @return
     */
    @PreAuthorize("hasAuthority('sys:res:update')")
    @PutMapping
    public String doUpdate(){
        return "Update Resource OK";
    }
    /**
     * 删除资源
     * @return
     */
    @DeleteMapping
    public String doDelete(){
        return "Delete resource ok";
    }
}

7.2.5第五步:配置资源认证授权规则ResourceConfig

/**
 * 思考?对于一个系统而言,它资源的访问权限你是如何进行分类设计的
 * 1)不需要登录就可以访问(例如12306查票)
 * 2)登录以后才能访问(例如12306的购票)
 * 3)登录以后没有权限也不能访问(例如会员等级不够不让执行一些相关操作)
 */
@Configuration
@EnableResourceServer //启动资源服务器的默认配置
//启动方法上的权限控制,需要授权才可访问的方法上添加@PreAuthorize等相关注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //super.configure(http);
        //1.关闭跨域攻击
        http.csrf().disable();
        //2.放行相关请求
        http.authorizeRequests()
                //配置炫耀认证的资源
                .antMatchers("/resource/**")
                .authenticated()
                //除了需要认证的资源,其他资源全部放行
                .anyRequest().permitAll();
    }
}

7.2.6第六步:配置令牌解析器对象TokenStore

/**
 * 在此配置类中配置令牌的生成,存储策略,验签方式(令牌合法性)。
 */
@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";
}

7.2.7第七步:启动Postman进行访问测试:

不携带令牌访问:

携带令牌访问:

没有访问权限:

8.网关工程设计及实现

8.1配置pom文件

<!--网关项目依赖-->
<dependencies>
    <!--网关服务依赖(底层netty技术+webflux技术),不能再有web依赖-->
    <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>

8.2创建bootstrap.yml配置文件并进行路由定义

#port
#spring.application.name
#nacos discovery and config
#sentinel
#gateway
#谓词逻辑返回值都为true则执行过滤器
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 #lb表示负载均衡,sso-resource为服务名
#          predicates:  #谓词对象,可以定义多个谓词逻辑,所有谓词逻辑返回值为true才会去执行filters
#            - Path=/sso/resource/**
#          filters:  #过滤器,是谓词逻辑的下一个执行步骤
#            - StripPrefix=1 #去掉path中的第一层目录
#        - id: router02 #认证服务器路由
#          uri: lb://sso-auth  #认证服务
#          predicates:
#            - Path=/sso/oauth/**
#          filters:
#            - StripPrefix=1
#      globalcors: #跨域配置(写到配置文件的好处是可以将其配置写到配置中心)
#        corsConfigurations: #所有跨域配置只是针对ajax请求,因为ajax请求不支持跨域
#          '[/**]':
#            allowedOrigins: "*"
#            allowedHeaders: "*"
#            allowedMethods: "*"
#            allowCredentials: true

8.3定义启动类

8.4HttpClient访问测试


9.客户端UI工程设计及实现

9.1编辑pom文件

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

9.2创建启动类

9.3创建UI工程登陆页面

第一步:在resource目录下创建static目录
第二步:在static目录下创建登陆页面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">
    <!--ElementUI CSS-->
    <link rel="stylesheet" href="https://unpkg/element-ui/lib/theme-chalk/index.css">
    <title>login</title>
</head>
<body>
<div class="container" id="app">
    <h3>Please Login</h3>
    <el-form :model="login1" label-width="80px">
        <el-form-item label="用户名">
            <el-input v-model="login1.username" placeholder="请输入用户名"></el-input>
        </el-form-item>
        <el-form-item label="密码">
            <el-input v-model="login1.password" type="password" placeholder="请输入密码">
            </el-input>
        </el-form-item>
        <el-row>
            <el-button type="primary" round @click="doLogin()">主要按钮</el-button>
        </el-row>
    </el-form>
<!--    <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>
<!--import Vue before Element-->
<script src="https://cdn.jsdelivr/npm/vue@2/dist/vue.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg/element-ui/lib/index.js"></script>
<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://unpkg/axios/dist/axios.min.js"></script>
<script>
    var vm = new Vue({
        el: "#app",//定义监控点,vue底层会基于此监控点在内存中构建dom树
        data() { //此对象中定义页面上要操作的数据
            return {
                login1: {
                    username: "",
                    password: ""
                }
            }
        },
        methods: {//此位置定义所有业务事件处理函数
            doLogin() {
                //1.定义url
                let url = "http://localhost:9000/sso/oauth/token"
                //2.定义参数
                let params = new URLSearchParams()
                params.append('username', this.login1.username);
                params.append('password', this.login1.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";
                        //启动一个定时器,一个小时以后,向认证中心发送刷新令牌
                        /*                        setTimeout(function () {

                                                })*/
                    })
                    .catch((e) => {
                        console.log(e);
                    })
            }
        }
    });
</script>
</body>
</html>

9.4创建资源展现页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <!--ElementUI CSS-->
    <link rel="stylesheet" href="https://unpkg/element-ui/lib/theme-chalk/index.css">
    <title>Title</title>
</head>
<body>
<div id="app">
    <h1>The Resource Page</h1>
    <el-button type="primary" plain @click="doSelect">查询我的资源</el-button>
    <el-button type="primary" plain @click="doUpdate">修改我的资源</el-button>
<!--    <el-button :plain="true" @click="doSelect">查询我的资源</el-button>
    <el-button :plain="true" @click="doUpdate">修改我的资源</el-button>-->
<!--    <button οnclick="doSelect()">查询我的资源</button>
    <button οnclick="doUpdate()">修改我的资源</button>-->
</div>
<!--import Vue before Element-->
<script src="https://cdn.jsdelivr/npm/vue@2/dist/vue.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg/element-ui/lib/index.js"></script>
<script src="https://unpkg/axios/dist/axios.min.js"></script>
<script>
    new Vue({
        el: "#app",
        data() {
            return{

            }
        },
        methods: {
            doSelect() {
                let url = "http://localhost:9000/sso/resource";
                //获取登录后,存储到浏览器客户端的访问令牌
                let token = localStorage.getItem("accessToken");
                //发送请求时,携带访问令牌
                axios.get(url, {headers: {"Authorization": "Bearer " + token}})
                    .then(response => {
                        this.$message({
                            showClose: true,
                            message: '恭喜你,查询成功',
                            type: 'success'
                        })
                        console.log(response.data);
                    })
                    .catch(function (e) {//失败时执行catch代码块
                        if (e.response.status == 401) {
                            this.$message({
                                showClose: true,
                                message: '请先登录',
                                type: 'warning'
                            })
                            location.href = "/login.html";
                        } else if (e.response.status == 403) {
                            this.$message({
                                showClose: true,
                                message: '您没有权限',
                                type: 'warning'
                            })
                        }
                        console.log("error", e);
                    })
            },
            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(response => {
                        this.$message({
                            showClose: true,
                            message: '恭喜你,更新成功',
                            type: 'success'
                        })
                        console.log(response.data);
                    })
                    .catch(e => {//失败时执行catch代码块
                        console.log(e);
                        if (e.response.status == 401) {
                            this.$message({
                                showClose: true,
                                message: '请先登录',
                                type: 'warning'
                            })
                            location.href = "/login.html";
                        } else if (e.response.status == 403) {
                            this.$message({
                                showClose: true,
                                message: '您没有权限',
                                type: 'warning'
                            })
                        }
                        console.log("error", e);
                    })
            }
        }
    })
</script>
</body>
</html>

9.5打开浏览器进行访问测试

admin 123456

登录后跳转


10.技术摘要应用实践说明

10.2Spring Security 技术

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

10.3Jwt数据规范

官方JWT规范定义,它构成有三部分,分别为Header(头部),Payload(负载),Signature(签名):

10.3.1Header部分

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子

{
  "alg": "HS256",
  "typ": "JWT"
}

alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(简写HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。最后,将这个 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。

10.3.2Payload部分

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。这个 JSON 对象也要使用 Base64URL 算法转成字符串。

10.3.3Signature部分

Signature 部分是对前两部分的签名,其目的是防止数据被篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

10.4Oauth2规范

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

11.SSO微服务工程中用户行为日志的记录

11.1系统需求分析

用户在sso-resource工程访问我们的资源数据时,获取用户的行为日志信息,然后传递给sso-system工程,将日志信息存储到数据库

11.2系统服务中的日志存储设计

系统服务负责将其它服务获取的用户行为日志写入到数据库。

11.2编辑pojo对象

/**
 * 基于此对象封装用户行为日志?
 * 谁在什么时间执行了什么操作,访问了什么方法,传递了什么参数,访问时长是多少.
 */
@Data
@TableName("tb_logs")
public class Log implements Serializable {
    private static final long serialVersionUID = 3054471551801044482L;
    @TableId(type = IdType.AUTO)
    private Long id;
    private String username;
    private String operation;
    private String method;
    private String params;
    private Long time;
    private String ip;
    @TableField("createdTime")
    private Date createdTime;
    private Integer status;
    private String error;
}

11.2.2编辑mapper层

/**
 * 用户行为日志的数据访问逻辑对象(DAO),
 * 通过此对象完成用户行为日志的持久化操作
 * */
@Mapper
public interface LogMapper extends BaseMapper<Log> {
}

11.2.3Service逻辑实现

public interface LogService {
    void insertLog(Log log);
}
@Service
public class LogServiceImpl implements LogService {
    @Autowired
    private LogMapper logMapper;
    /**
     *写日志逻辑,并且希望这个写日志的动作通过一个异步线程去执行
     * (因为用户不关心底层日志记录),这个线程不占用web服务(tomcat)的线程资源
     * @Async 注解描述方法时,此方法为一个异步切入点方法,
     * 此方法会有一个spring内置的线程池中的线程调用执行,
     * 注意: 描述的方法不能有返回值
     *       需要通过@EnableAsync注解对项目的启动类或者配置类进行描述,启动异步机制
     */
    @Async
    @Override
    public void insertLog(Log log) {
        logMapper.insert(log);
    }
}

11.2.4Controller 逻辑实现

@RestController
@RequestMapping("/log")
public class LogController {
    @Autowired
    private LogService logService;

    @PostMapping
    public void doInsertLog(@RequestBody Log log){
        logService.insertLog(log);
    }
}

11.2.5HttpClient测试


11.3资源服务中行为日志操作设计

在不修改目标业务方法代码实现的基础之上,访问目标方法时,获取用户行为日志

11.3.1编辑pojo对象

/**
 * 基于此对象封装用户行为日志?
 * 谁在什么时间执行了什么操作,访问了什么方法,传递了什么参数,访问时长是多少.
 */
@Data
public class Log implements Serializable {
    private static final long serialVersionUID = 8687158282896650331L;
    private Long id;
    private String username;
    private String operation;
    private String method;
    private String params;
    private Long time;
    private String ip;
    private Date createdTime;
    private Integer status;
    private String error;
}

11.3.2切入点注解定义

构建一个自定义注解,名字为RequiredLog,后续会基于此注解描述作为切入点,定义切入点方法

/**
 * 希望由此注解描述的方法为日志切入点记录
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiredLog {
    String value() default "";
}

11.3.3在ResourceController使用自定义注解

查询资源:@RequiredLog 注解描述的方法为一个日志切入点方法,访问此方法进行日志写入

/**
 * 查询资源
 * @RequiredLog 注解描述得方法为一个日志切入点方法,访问此方法进行日志写入
 */
@RequiredLog("查询资源")    //自定义注解
@PreAuthorize("hasAuthority('sys:res:list')")//描述的是一个切入点方法,访问此方法,需加此权限
@GetMapping
public String doSelect(){
    return "Select Resource ok";
}

11.3.4AOP方式获取并记录日志

需要在bootstrap.yml文件加日志:

定义一个日志切面,基于此切面中的通知方法实现用户行为日志的获取和记录

/**
 * 自定义用户行为日志切面
 */
@Aspect
@Component
public class LogAspect {
    /**
     * 切入点,采用注解方式的切入点表达式,
     * 使用RequiredLog注解描述方法时,被描述的方法就是切入点方法
     * doLog方法,启动了承载@Pointcut注解的作用
     */
    @Pointcut("@annotation(com.jt.resource.annotation.RequiredLog)")
    public void doLog() {
    }

    /**
     * 通知方法,在此方法内,可以手动调用目标方法执行链,在执行链执行过程中获取用户行为日志,进行封装和记录,
     * ProceedingJoinPoint为连接点对象这个类型对象只能应用在@Around注解描述的方法上,并且可以通过此连接点对象
     * 获取目标方法信息,调用目标方法执行链,目标方法(切入点方法)执行结果
     */
    @Around("doLog()")
    public Object doAround(ProceedingJoinPoint jp) throws Throwable {
        int status = 1;//状态码
        String error = null; //错误信息
        long time = 0l;//执行时长
        //获取目标方法开始执行时间
        long t1 = System.currentTimeMillis();
        System.out.println("Before:" + t1);
        try {
            //调用目标方法执行链,这个执行链的终端是切入点方法
            Object result = jp.proceed();
            //获取目标方法执行结束时间
            long t2 = System.currentTimeMillis();
            System.out.println("After:" + t2);
            time = t2-t1;
            return result;
        } catch (Throwable e) {
            long t3 = System.currentTimeMillis();
            System.out.println("Exception:" + t3);
            time=t3-t1;
            status=0;
            error=e.getMessage();
            throw e;
        }finally {
            //无论目标方法是否执行成功,都要记录日志
            saveLog(jp,time,status,error);
        }
    }
    //存储用户行为日志
    private void saveLog(ProceedingJoinPoint jp,long time,int status,String error) throws Throwable {
        //1.获取用户行为日志
        //1.1获取目标对象类型(切入点方法所在类的类型)
        Class<?> targetClass = jp.getTarget().getClass();
        //1.2获取目标方法(切入点方法)
        //1.2.1获取方法签名(包含方法信息)
        //通过连接点对象获取方法签名对象(此对象包含目标方法的信息)
        MethodSignature signature = (MethodSignature)jp.getSignature();
        //1.2.2获取方法对象
        Method targetMethod = targetClass.getDeclaredMethod(signature.getName(), signature.getParameterTypes());
        //1.3获取方法上的RequiredLog注解内容,获取用户操作
        //1.3.1获取目标方法上的注解
        RequiredLog requiredLog = targetMethod.getAnnotation(RequiredLog.class);
        //1.3.2获取注解中的内容,执行了什么操作
        String operation = requiredLog.value();
        //1.4获取目标方法名(类名+方法名)
        String targetMethodName = targetClass.getName() + "." + targetMethod.getName();
        //1.5获取目标方法执行时传入的参数
        String params = new ObjectMapper().writeValueAsString(jp.getArgs());
        //1.6获取登录用户名,
        String username = (String) SecurityContextHolder.getContext()
                .getAuthentication()//认证对象
                .getPrincipal();
        //1.7获取ip地址,ServletRequestAttributes里边才有getRequest().getRemoteAddr()方法,所以强转
        ServletRequestAttributes requestAttributes =
                (ServletRequestAttributes ) RequestContextHolder.getRequestAttributes();
        String ip = requestAttributes.getRequest().getRemoteAddr();

        //2.将用户行为日志,封装到Log对象
        Logger log = LoggerFactory.getLogger(LogAspect.class); //@Slf4j 效果一样
        Log logInfo = new Log();
        logInfo.setIp(ip);
        logInfo.setUsername(username);
        logInfo.setOperation(operation);
        logInfo.setMethod(targetMethodName);
        logInfo.setParams(params);
        logInfo.setTime(time);
        logInfo.setError(error);
        logInfo.setStatus(status);
        logInfo.setCreatedTime(new Date());
        //3.记录用户行为日志(控制台,文件,数据库)
        log.debug("==="+logInfo+"===");
        remoteLogService.insertLog(logInfo);
    }

本文标签: 单点系统sso