0
点赞
收藏
分享

微信扫一扫

SpringCloud-Day08单点登录系统

Sophia的玲珑阁 2022-01-10 阅读 60

单点登录系统

1.sca-system工程

这个工程在于从数据库中根据Id查到其权限,根据usename查到数据库有无用户。这部分和第三阶段很像。

(1)pom文件,里面最重要的就是数据库的连接依赖和mybatis-plus依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>02-sca</artifactId>
        <groupId>com.jt</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>sca-system</artifactId>
    <!--1.数据库访问相关-->
    <!--1.1 mysql 数据库驱动-->
    <dependencies>
        <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 服务相关 自带tomcat 服务器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--限流依赖 需要限流-->
<!--        <dependency>-->
<!--            <groupId>com.alibaba.cloud</groupId>-->
<!--            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>-->
<!--        </dependency>-->
    </dependencies>
</project>

(2)配置文件bootstrap.yml文件,里面最重要的就是添加数据源

#定义端口号
server:
  port: 8061
#定义在配置中心的名字
spring:
  application:
    name: sso-system
#定义nacos注册中心,nacos配置中心
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yml
#定义数据源
  datasource:
    url: jdbc:mysql://localhost:3306/jt-sso?serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: root
#mybatis-plus:
#  mapper-locations: classpath:/*.xml

logging:
  level:
    com.jt: debug

(3)pojo对象—>封装从数据里面得到的字段信息

首先明确:对象用来存储数据,方法实现逻辑。对于我们需要封装数据的对象,都会实现Serilizable接口,并且会生成一个固定的UID。

package com.jt.system.pojo;

import lombok.Data;

import java.io.Serializable;

/**
 * 添加@Data为了不写toString getter setter方法,值得注意的是:
 * setter方法不会为final属性创建setter方法
 * @Data在编译器有效
 * idea---->jdk
 * idea---->lombok进行编译
 */
@Data
//@TableName("tb_user")假如sql语句自己写,不需要通过@TableName指定表名
public class User implements Serializable {
   private static final long serialVersionUID = 4831304712151465443L;
    /**
     * 通过反射技术对象创建,通过set方法
     * 什么地方用到序列化?为什么要固定UID
     * Serializable起到标识性的作用
     * 以后记住:用来存储数据的对象,都建议实现序列化接口,并且添加一个序列化id
  
  • 可以参考String Integer ArrayList。。。
    • 为什么要添加UID?因为这样之后如果改变类的结构,也可以反序列化成功
    • 从数据库取出封装pojo
      */
      private Long id;
      private String username;
      private String password;
      private String status;

}

1)如何生成一个UID?
在这里插入图片描述
在这里插入图片描述
2)lombok注解的使用
1.@Data注解会为该pojo对象自动生成,getter,setter,toString方法,需要注意的是setter方法不会为final修饰的属性生成setter方法
2.@Data注解只在编译期有效
3).pojo对象是spring通过反射技术创建,通过set方法将数据库中的信息封装到对象的属性上面

(4)关于dao层

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
/**
 * 为什么要继承BaseMapper接口呢?答:不写也可以,只是为了使用BaseMapper里面的方法
 * @Mapper注解由myBaytis提供,交给spring容器管理,创建代理对象(对象是sqlSesionTemplate)
 * BaseMapper支持单表查询,可以使用其中的对象
 * 自己写实现类?
 */
public interface UserMapper extends BaseMapper<User> {
    /**
     * 为什么要加引号呢?
     * @param username
     * @return
     */
    @Select("select *"+"from tb_users"+" where username = #{username}")
    User selectUserByUsername(String username);

    /**
     * 根据用户id,找到该id下的用户得到的许可
     * @param userId
     * @return
     */
    @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);
}

(1) 三表联查
首先理解表与表之间的关系:
一对多,jt-sso.tb_users 和jt-sso.tb_logs,在日志上进行关联
多对多,新增一张关系表进行关联
#方案一
select role_id from tb_user_roles where id = 1;
select menu_id from tb_role_menus where role_id in (1);
select permission from tb_menus where id in (1,2,3);

#方案二
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 id = 1));

#方案三,总结:先找出根据什么(最初始)找到什么(最终),剩下的通过from join on join on,将表关联起来就行。
select permission from tb_menus m join tb_role_menus rm on
m.id = rm.menu_id join tb_user_roles ur on ur.role_id =rm.role_id
where ur.id =1;
(2)@Mapper
加上此注解之后,首先通过Proxy.Instance创建一个SqlSession的代理对象,该代理对象执行excutor的方法,对于数据库进行访问。
(3)数据库中有缓存也有拦截器
(4)数据库中设计的设计模式
装饰模式(n缓存)、模板模式(SqlSessionTemplate)、责任链模式(拦截器),工厂模式,代理模式单例设计模式
(5)extends BaseMapper
使用myBatis-plus,这样就可以使用myBatis-plus中的对象去执行sql语句,需要注意:myBatis-plus只支持单表操作,多表操作无能为力
(6)自己创建mapper的实现类,并且交给spring管理
1.接口

package com.jt.system.dao;

import com.jt.system.pojo.User;

public interface UserDao {
    User selectUserByUserName(String username);
}

2.实现类,需要交给spring容器管理

import com.jt.system.dao.UserDao;
import com.jt.system.pojo.User;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class UserDaoImpl implements UserDao {
    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;
    @Override
    public User selectUserByUserName(String username) {
        User user = sqlSessionTemplate.selectOne("com.jt.system.dao.UserDao.selectUserByUserName",username);
        return user;
    }
}

3.映射文件
映射文件路径:
在这里插入图片描述
映射文件内容:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.jt.system.dao.UserDao">
    <select id="selectUserByUserName" resultType="com.jt.system.pojo.User">select * from tb_users where username = #{username} </select>
</mapper>

4.配置文件内容:

#mybatis-plus:
#  mapper-locations: classpath:/*.xml

(7) 关于数据库的连接
与数据库进行连接时,必定要经过TCP的三次握手,四次挥手,非常消耗时间,所以把与数据库的连接放在连接池中。
1)connection里面的设计模式:单例模式,享元模式,桥接模式,门面模式
2)java连接池都要根据DataSource规范,数据库的规范就是javax.sql.DataSource,基于此规范创建了一个HaKariDataSource,通过此对象创建HaKariPool(CopyOnWriteArrayList存储)

@SpringBootTest
//测试类必须在主启动类的同包或者子包中
public class DataSourceTests {
    /**
     * 这里的DataSource是一个数据标准或者规范,Java所有连接池需要基于这个规范进行实现,
     * 我们项目中添加了一个spring-boot-start-jdbc依赖后,系统会自动帮我们引入一个HikariCp连接池
     * 这个连接中池有一个HikariDataSource,对象就是基于javax.sql.DataSource规范落地
     * 这个对象在SpringBoot工程启动,进行自动配置(DataSourceAutoConfiguration)
     */
    @Autowired
    private DataSource dataSource;
    /**
     * 通过反射获得的jdk代理对象
     */
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private UserDao userDao;
    @Test
    void testGetConnection() throws SQLException {
        /*
            通过dataSource获取连接时,首先获取的是连接池HikariPool(这个池通过CopyOnWriteArrayList存储),
            然后从池中获取连接,这个连接需要TCP连接这里有三个设计模式,单例模式,享元模式,桥接模式,门面模式
         */
        Connection conn=
                dataSource.getConnection();//简单工厂模式
        System.out.println(conn);
    }

(8)断言
Assert.notNull(user, “user is null”);//spring包下面的,一般业务使用
Assertions.assertNotNull(user,“user is null”);//util包下面,一般单元测试

  @Test
    void testSelectUserByUsername(){
        User user =
                userMapper.selectUserByUsername("admin");
        // System.out.println(user);
        /**
       	 * 做单元测试时,一般不用System,使用断言
         * 这里测试user是否不为空。如果为空,抛出异常,异常的提示信息为message
         */
        Assert.notNull(user, "user is null");//spring包下面的,一般业务使用
        Assertions.assertNotNull(user,"user is null");//util包下面,一般单元测试       

2.sca-auth工程

(1) 创建pojo对象,封装从sca-system里面查询的user对象

import lombok.Data;

import java.io.Serializable;
@Data
/**
 * 这个User用来封装从sca-System的controller里面的返回的user对象
 */
public class User implements Serializable {
    private static final long serialVersionUID = 3570548663999909287L;
    private Long id;
    private String username;
    private String password;
    private String status;
}

(2)通过figen调用远端服务接口
1)调用figen要有两个注解@FeignClient,@EnableFeignClients
2)Spring内部通过Proxy.instance创建代理对象,内部使用调用代理对象
3)一定要使用@PathVariable(“username”)
4)通过@PathVariable(“username”) 的参数,给到restFull里面,然后通过此路径调用远端服务器。

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")
/**
 * 使用Fegin会在底层创建一个jdk代理对象
 * 使用Fegin对象就可以自动访问sso-system里面的方法
 * 如果不写contextId,默认就是sso-system
 */
public interface RemoteUserService {
    //注意这里面的路径信息要与controller里面的信息相同
    @GetMapping("/user/login/{username}")
    //注意fegin里面必须要写@PathVariable("username")
    User selectUserByUsername(@PathVariable("username") String username);
    @GetMapping("/user/permission/{userId}")
    List<String> selectUserPermissions(@PathVariable("userId") Long userId);
}

(3)UserDetailsServiceImpl
有两个对象一致的需要指定,这个返回值交给Spring security认证中心进行比对(分析),就是和数据库中的信息和现在的userinfo比对
* 如果能匹配,那么就登录成功,如果不匹配那么就登录失败
* 注意此处没有对密码进行加密
package com.jt.auth.service;
import org.springframework.security.core.userdetails.User;
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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
@Service
@Slf4j
/**

  • 通过fegin接口->远端->数据库
    /
    public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private RemoteUserService remoteUserService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    /
    *
    * 调用feign,传入一个username,然后给restFull路径,之后根据路径调用sca-system-controller
    /
    com.jt.auth.pojo.User user=
    remoteUserService.selectUserByUsername(username);
    if(user==null)
    throw new UsernameNotFoundException(“用户不存在”);
    //2.基于用于id查询用户权限
    List permissions=
    remoteUserService.selectUserPermissions(user.getId());
    /
    *
    * 防止出错,所以用日志
    /
    log.debug(“permissions {}”,permissions);
    //3.对查询结果进行封装并返回
    /
    *
    * 有两个对象一致的需要指定,这个返回值交给Spring security认证中心进行比对(分析),就是和数据库中的信息和现在的userinfo比对
    * 如果能匹配,那么就登录成功,如果不匹配那么就登录失败
    * 注意此处没有对密码进行加密
    */
    User userInfo= new User(username,
    user.getPassword(),
    AuthorityUtils.createAuthorityList(permissions.toArray(new String[]{})));
    return userInfo;
    //交给这个对象AuthenticationManager进行比对
    }
    }
    (4)配置中心
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
/*
    为什么使用那个路径去访问,为什么使用post请求
 */

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

    /**
     * 初始化加密对象
     * 此对象提供了一种不可逆的加密方式,相对于md5方式会更加安全
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 定义认证管理器对象,这个对象负责完成用户信息的认证,
     * 即判定用户身份信息的合法性,在基于oauth2协议完成认
     * 证时,需要此对象,所以这里讲此对象拿出来交给spring管理
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManagerBean()
            throws Exception {
        return super.authenticationManager();
    }

    /**配置认证规则
     * 此方法为http请求配置方法,可以在此方法中进行配置
     * 1)哪些资源不用登录就可以访问
     * 2)哪些资源需要登录才可以访问
     * 不做任何配置,所有资源都可以访问。
     * */
    @Override
    protected void configure(HttpSecurity http)
            throws Exception {
        //http.authorizeRequests().antMatchers("/**").authenticated();//表示所有资源都要进行登录才可以访问
        //http.authorizeRequests().antMatchers("/default.html").authenticated().anyRequest().permitAll();
        //表示除了default.html需要认证,其他都放行
        // http.authorizeRequests()
        // .anyRequest().permitAll();//2.放行所有资源的访问(后续可以基于选择对资源进行认证和放行)
        //super.configure(http);//默认所有请求都要认证,注释掉之后表示把默认的页面给关了,所有请求都放行
        //1.禁用跨域攻击(先这么写,不写会报403异常),加入没有禁用,使用postman或者httpclient会有404没有login,因为没有对于login进行配置
        //登录成功之后跳转到那个页面(转发之前是)
        //http.formLogin().successForwardUrl("/index.html").failureForwardUrl("/default.html");
        /**
         *  http.csrf().disable();防止第三方工具使用post进行跨域攻击
         */
        http.csrf().disable();//1.禁用跨域攻击(浏览器访问不会异常,第三方访问会403异常),加入没有禁用,
        http.authorizeRequests()
                .anyRequest().permitAll();//2.放行所有资源的访问(后续可以基于选择对资源进行认证和放行)
        //3.自定义定义登录成功和失败以后的处理逻辑(可选)
        //假如没有如下设置登录成功会显示404,这个是转发
        /**
         * 这种用于前后端分离的项目,最后会返回一个JSON
         */
        http.formLogin()//这句话会对外暴露一个登录路径/login
                .successHandler(successHandler())
                .failureHandler(failureHandler());
        //http.formLogin().defaultSuccessUrl("/index.html");
        // 这个方式默认的是重定向,请求和转发一般都不用前后端分离的项目,这种跳转不成功,会返回login

    }
    //定义认证成功处理器
    //登录成功以后返回json数据
    /*
    加上@Bean注解以后,对象只会创建一次
     */
    @Bean
    public AuthenticationSuccessHandler successHandler(){
        //lambda,其中authentication表示认证信息
        return (request,response,authentication)->{
            //构建map对象封装到要响应到客户端的数据
            Map<String,Object> map=new HashMap<>();
            map.put("state",200);
            map.put("message", "login ok");
            //将map对象转换为json格式字符串并写到客户端
            writeJsonToClient(response,map);
            /**
             * 这样返回的都是200
             */
//           PrintWriter writer = response.getWriter();
//            writer.println("success");
//            writer.flush();
//
        };
    }
    //定义登录失败处理器
    @Bean
    public AuthenticationFailureHandler failureHandler(){
        return (request,response,exception)->{
            //构建map对象封装到要响应到客户端的数据
            Map<String,Object> map=new HashMap<>();
            map.put("state",500);
            map.put("message", "login error");
            //将map对象转换为json格式字符串并写到客户端
            writeJsonToClient(response,map);
        };

    }
    private void writeJsonToClient(
            HttpServletResponse response,
            Map<String,Object> map) throws IOException {
        //将map对象,转换为json
        String json=new ObjectMapper().writeValueAsString(map);
        //设置响应数据的编码方式,mysql里面没有-,但是java里面有-
        response.setCharacterEncoding("utf-8");
        //设置响应数据的类型
        response.setContentType("application/json;charset=utf-8");
        //将数据响应到客户端
        PrintWriter out=response.getWriter();
        out.println(json);
        out.flush();
    }
}
举报

相关推荐

0 条评论