目录
一、使用网关验证登录并授权【续】
7.使用Spring Security实现登录
在使用Spring Security实现登录验证时,如果需要使用 UserDetailsService ,必须自定义一个 WebSecurityConfigurerAdapter 的子类实现配置,则在 cn.tedu.straw.gateway.security 包中创 建 WebSecurityConfigurer 类进行配置:
然后,还需要在 StrawGatewayApplication 启动类上添加 @EnableWebSecurity 注解:
完成后,启动当前项目,通过 http://localhost/login 即可测试登录,正确的用户名和密码就是当前数据 表中的用户名和密码!
二、完善注册功能
由于注册的具体数据处理都是在 straw-api-user 项目中实现的(控制器层、业务层、持久层的功能,全 部在该功能中),该项目是添加了Spring Security的依赖,并且没有作过任何与Spring Security相关的配置(此前在练习时写过配置类,但是已经去除注解,将不会生效),在默认情况下,所有的访问都是 要求登录的,如果没有登录,则不允许访问该服务器,虽然可能已经通过网关实现了登录,但是,它会认为是在 straw-gateway 的服务器登录的,与 straw-api-user 的服务器无关,所以,如果尝试注册,将失败!
为了实现“通过 straw-gateway 的项目能够提交请求到 straw-api-user 的项目的控制器”,需要在 straw-api-user 项目中关闭跨域攻击(否则会导致401错误),并且允许所有访问(无论是注册,还是 后续的任何功能,都直接允许,因为是否允许访问应该在网关进行控制,只要能够通过网关,后续的服 务器是不需要验证登录或其它身份的)。
由于“注册”时将提交异步请求,所以,先在 straw-gateway 项目中关闭跨域攻击,并且,如果仅关闭跨域攻击,会导致任何请求都不需要登录,则还需要设置验证请求并使用登录表单,在 straw-gateway 的 WebSecurityConfigurer 中添加配置:
然后在 straw-api-user 的 cn.tedu.straw.api.user.security 包中创建 WebSecurityConfigurer 类进行配置:
并确保在 StrawApiUserApplication 类的声明之前存在 @EnableWebSecurity 注解:
完成后,先重启 straw-api-user 项目,再重启 straw-gateway 项目,打开浏览器,先通过 http://local host 或 http://localhost/login 登录,然后,再通过 http://localhost/register.html 即可实现注册! 目前,在执行“注册”之前还需要先“登录”是不合理的,应该将这些页面设置为不需要登录即可访问!所有 的访问控制可在 straw-gateway 进行配置,所以,在 straw-gateway 的 WebSecurityConfigurer 中补 充:
完成后,重启 straw-gateway 项目,推荐清除浏览器缓存,通过 http://localhost/register.html 即可打 开注册页面,并且可以成功使用注册功能。
三、使用自定义的登录页面
Spring Security是内置登录页面的,也可以自行指定登录页面,在 WebSecurityConfigurerAdapter 的 子类中进行配置时,可以在“启用登录页面”之后,调用相关的方法进行设置即可!
关于Spring Security应用的登录页面,必须:
- 必须使用 <form action="/login" method="post"> 表单配置;
- 用户名和密码必须使用 username 和 password 作为请求参数名称。
四、标签列表---持久层
如果要查询所有的标签的数据,得到标签列表,需要执行的SQL语句大致是:
SELECT id, name FROM tag ORDER BY id;
接下来,创建新的子模块项目 straw-api-question ,将使用该功能来开发:
其实,直接在原本的 straw-api-user 或其它项目中开发该功能是完全可以的,为了更加真实的模 拟实际的分布式开发,所以,将“问题”相关的功能使用专门的项目来开发,以暴露更多开发分布式 项目可能出现的问题,并解决相关问题。
当项目创建出来后,调整 pom.xml 文件:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.tedu</groupId>
<artifactId>straw</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>cn.tedu</groupId>
<artifactId>straw-api-question</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>straw-api-question</name>
<dependencies>
<!-- Straw Commons -->
<dependency>
<groupId>cn.tedu</groupId>
<artifactId>straw-commons</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Lombok:通过注解简化开发 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- druid:alibaba的数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- web:允许项目启动在Tomcat -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
然后,在 application.properties 中添加配置:
# 显式的配置当前项目部署到服务器时运行在哪个端口号
server.port=8081
# 应用程序名称
spring.application.name=api-question
# 连接数据库的配置信息
spring.datasource.url=jdbc:mysql://localhost:3306/straw?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.initial-size=2
spring.datasource.druid.max-active=100
spring.datasource.druid.min-idle=2
# 配置SQL语句的XML文件的位置
mybatis.mapper-locations=classpath:mapper/*.xml
先在straw-commons
中创建封装本次查询结果的VO类,在cn.tedu.straw.commons.vo
包中创建TagVO
类,并在这个类中声明id
、name
这2个属性:
package cn.tedu.straw.commons.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
public class TagVO implements Serializable {
private Integer id;
private String name;
}
在straw-api-question
中cn.tedu.straw.api.question.mapper
创建TagMapper
持久层接口文件,在接口中声明“查询标签列表”的抽象方法:
package cn.tedu.straw.api.question.mapper;
import cn.tedu.straw.commons.vo.TagVO;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface TagMapper {
/**
* 查询所有的标签数据
*
* @return 所有标签的集合
*/
List<TagVO> findAll();
}
然后,在启动类的声明之前添加@MapperScan
注解,配置接口文件所在的包:
然后,在resources
下创建mapper
文件夹,并在这个文件夹中创建(或复制粘贴得到)TagMapper.xml
文件,配置以上接口中的抽象方法映射的SQL语句:
<?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="cn.tedu.straw.api.question.mapper.TagMapper">
<select id="findAll" resultType="cn.tedu.straw.commons.vo.TagVO">
SELECT id, name FROM tag ORDER BY id
</select>
</mapper>
完成后,先在application.properties
中设置日志的显示级别:
# 日志的显示级别
logging.level.cn.tedu.straw=trace
在test
文件夹(不存在则创建)下的cn.tedu.straw.api.question.mapper
包中创建TagMapperTests
测试类,测试以上方法:
package cn.tedu.straw.api.question.mapper;
import cn.tedu.straw.commons.vo.TagVO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
@Slf4j
public class TagMapperTests {
@Autowired
TagMapper mapper;
@Test
void findAll() {
List<TagVO> tags = mapper.findAll();
log.debug("标签数量:" + tags.size());
for (TagVO tag : tags) {
log.debug(">>> " + tag);
}
}
}
五、标签列表---业务层
在cn.tedu.straw.api.question.service
包下创建ITagService
接口,并在接口中定义业务的抽象方法:
package cn.tedu.straw.api.question.service;
import cn.tedu.straw.commons.vo.TagVO;
import java.util.List;
public interface ITagService {
/**
* 查询所有的标签数据
*
* @return 所有标签的集合
*/
List<TagVO> getTagList();
}
然后,在cn.tedu.straw.api.question.service.impl
包下创建TagServiceImpl
类,在类的声明之前添加@Service
注解,实现ITagService
接口,重写业务方法:
package cn.tedu.straw.api.question.service.impl;
import cn.tedu.straw.api.question.mapper.TagMapper;
import cn.tedu.straw.api.question.service.ITagService;
import cn.tedu.straw.commons.vo.TagVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class TagServiceImpl implements ITagService {
@Autowired
TagMapper tagMapper;
@Override
public List<TagVO> getTagList() {
return tagMapper.findAll();
}
}
完成后,在test
的cn.tedu.straw.api.question.service
包下创建TagServiceTests
测试类,编写并执行单元测试:
package cn.tedu.straw.api.question.service;
import cn.tedu.straw.commons.vo.TagVO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
@Slf4j
public class TagServiceTests {
@Autowired
ITagService service;
@Test
void getTagList() {
List<TagVO> tags = service.getTagList();
log.debug("标签数量:{}", tags.size());
for (TagVO tag : tags) {
log.debug(">>> {}", tag);
}
}
}
六、关于Redis
Redis是一个基于内存缓存数据的NoSQL服务器。
把数据在Redis服务器中也存储一份,当需要查询数据时,不再从MySQL服务器查询,而是从Redis服务器查询,可以非常有效的提高查询效率!
注意:缓存的数据可能是不准确的,所以,一般,只有不要求十分精准,并且访问频率高的数据,才会放在缓存中!
在安装Redis时,建议勾选:
并保持勾选“在防火墙中添加例外”:
安装完成后,重启整个IDEA软件,在IDEA的终端中,输入 redis-cli (表示redis client)即可登录 Redis控制台,并且,可以输入 ping 指定以测试Redis服务是否处于正常运行状态,将得到 PONG 反馈:
关于Redis的常用指令可参考:https://www.cnblogs.com/cxxjohnson/p/9072383.html
附(一):关于Slf4j
Slf4j是项目中用于在控制台输入日志的,当项目中添加了Lombok依赖后,在任何类的声明之前添加@Slf4j
,就在可以当前类的任意位置使用log
属性调用相关方法来输出日志(该属性不需要声明,Lombok会自动像添加SET/GET方法一样,在编译时介入,并声明该属性),例如:
package cn.tedu.straw.api.question.mapper;
import cn.tedu.straw.commons.vo.TagVO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
@Slf4j
public class TagMapperTests {
@Autowired
TagMapper mapper;
@Test
void findAll() {
List<TagVO> tags = mapper.findAll();
log.debug("标签数量:" + tags.size());
for (TagVO tag : tags) {
log.debug(">>> " + tag);
}
}
}
在Slf4j中,日志区分为几个级别,从轻微到严重分别是:
trace
:跟踪信息;debug
:调试;info
:一般信息;warn
:警告;error
:错误;fatal
:崩溃。
默认情况下,项目中的日志级别是info
,则较低级别trace
、debug
的日志不会被显示在控制台!而较高级别的默认是显示的,也就是info
、warn
、error
级别的日志将被显示!
将日志划分级别的好处在于可以随时输出各种信息,包括涉及项目的运行流程、关键数据的信息,这些信息可能只在开发阶段用于调试或测试功能时需要观察,在项目开发完成之后这些信息就不应该被显示出来了!如果使用System.out.println()
输出语句来显示这些信息的话,无论何时这些信息都将被显示!但是,使用Slf4j的话,可以将开发阶段的日志通过debug
级别进行输出,在开发完成后,设置为“仅显示info
或更高级别”即可。
在项目的application.properties
中,可以通过logging.level
来配置日志的显示级别,例如:
# 日志的显示级别
logging.level.cn.tedu.straw=trace
以上代码就表示将cn.tedu.straw
这个包及其子孙包中所有类输出的日志的显示级别都设置为trace
,则trace
级别及比它更高的级别的日志都将被输出!
如果只要某个类需要配置日志的显示级别,可以将以上配置的属性精确到该类:
# 日志的显示级别
logging.level.cn.tedu.straw.api.question.mapper.TagMapper=trace
另外,warn
或更高级别都意味着程序中有明显的问题需要关注,所以,是默认始终显示的!
附(二):相关软件
Redis、Elasticsearch、Kafka
以下链接是我的百度云盘,可以自行下载
链接: https://pan.baidu.com/s/1R_5YFXns-IzmrsiYjrW89Q
提取码: kw9p
附(三):关于序列化
在Java中,如果创建的某个类就只是在其中声明一些属性,并定义对应的SET/GET方法,或继续添加hashCode()
、equals()
、toString()
方法,这样的类都应该实现Serializable
接口。
序列化的根本原因在于CPU的单次处理数据的能力非常低下!目前,主流的CPU是64位的CPU,这里的“64位”表示CPU的“步长”,是CPU运行一次可以处理的二进制数的长度,也就是说,64位的CPU每运行一次,可以处理64个二进制位,也就是8个字节,可以换算成2个int
值,或1个long
值,或4个char
值……由此可见,CPU的单次处理数据的能力是非常弱的,之所以现在的CPU可以表现出很强的性能,是CPU的工作频繁很高,例如2.4G
完整的表示应该是2.4GHz
,也就是说,虽然一次处理不了多少数据,但是,运算频率非常高,所以,整体性能很强!
如果数据需要传输或克隆,由于CPU极有可能无法一次性完成处理,就需要将这些数据进行分批处理,在分批传输或克隆时,就需要关注传输的顺序,例如某个User
类型的数据中包含String username
、String password
、int age
这几个属性及值,在传输或克隆时,到底是先处理String username
还是int age
就是个问题!
其实,无论是先处理String username
还是先处理int age
都是可行的,只要传输或克隆的起始位置和接收位置保存一致即可!也就是说:发送方先发出String username
,接收方收到数据后将这部分数据视为String username
属性的值即可,不要出现发出的是String username
却被作为String password
的值来处理就可以了!
所以,具体的先后顺序并不重要,但是,必须存在这个顺序的约定,以保证发送与接收是一致的(毕竟计算机中没有“自动识别”相关的机制)!这个约定可以理解为是一套“序列化的方案”,一旦制定了该标签,发送方和接收方都按这个标准来执行,就可以保证传输或克隆的数据不会出错,不会出现发出String username
却把值放到String password
中这种张冠李戴的问题!
由于一个项目中可能存在多种这样的类型,而每一种类型中,无论多少个对象,序列化的方案可以使用同一个!所以,应该为每一套序列化方案给出一个唯一标识,以便于Java直接使用!
所以,在Java中,实现Serializable
接口,起到的作用就是作一个“标识”,明确的表示这个类是“可以被序列化的”,而自动生成的序列化版本uid就是这种类序列化方案的唯一标识!
在Java中,关于序列化的处理,除了应该实现Serializable
接口并生成序列化版本uid(当类的成员发生变化后应该重新生成该uid)以外,是不需要手动处理的,全部由Java自动完成!(当然,这一点也不一定是优点,在一些硬件性能非常弱的智能设备中,可能会设计其它的手动序列化机制)。