单点登录系统
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();
}
}