目录
一、使用网关验证登录并授权
1.思路
网关是服务器的集群中的第一入口,也是唯一入口,应该在这个入口就直接检查用户的登录信息,如果登录成功,授权也应该直接完成,而不是在集群中其它的服务器中进行登录验证和授权,因为如果用户登录信息有误,一开始都不允许执行到集群之内,而是在网关就直接回绝用户的请求!所以,网关straw-gateway
项目不仅仅要起到路由转发的作用,还需要完成登录验证和授权。
目前,已经在straw-api-user
项目中添加使用Spring Security的代码,这些代码都是练习Spring Security的功能而存在的,先将这些类删除,或使之不可用,例如去除类之前注解:
2.网关配置
首先,需要在straw-gateway
项目中添加依赖Spring Security框架,同时,由于验证登录时需要访问数据库(从数据库中查询尝试登录的用户的信息),所以,还需要在straw-gateway
项目中添加与数据库编程相关的依赖:
<dependencies>
<!-- Spring Security:安全框架,用于验证登录、授权访问、密码加密 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Lombok:通过注解简化开发 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Mybatis Plus:简化Mybatis开发 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</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>
<!-- 网关路由 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- Eureka Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</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>
由于添加了数据库的相关依赖,SpringBoot在启动时会自动读取连接数据库的信息,但是,当时straw-gateway
项目中并没有这些配置,所以,启动straw-gateway
就会报错!需要先在application.properties
中补充这些配置(从straw-api-user
中复制过来即可):
在straw-gateway
中处理登录时,必须要从数据库中查询用户信息,也就是处理User
类型的数据,而这个类型是在straw-api-user
中也需要使用到的,也就是2个项目都需要使用User
类型!其实,以后,其它的类也可能出现类似的情况,同一个类在多个不同的子模块项目中都需要被使用!
为了更好的解决这个问题,应该将这些类放在专门的项目中,当其它项目需要使用到这些类时,直接依赖该项目即可!
则创建straw-commons
子模块项目,用于存放公共使用的类(创建过程中,不需要勾选任何依赖):
创建成功后,由于当前项目只是用于存放公共使用的类,所以,并不需要独立运行,也不会在其中编写功能性代码,相关的文件是可以删除的:
然后,将straw-api-user
的model
包整个的复制到straw-commons
项目中:
当把model
包复制过来后,其中的类是报错的!因为当前straw-commons
项目并没有依赖必要的依赖,所以,还需要调整straw-commons
的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>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>straw-commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>straw-commons</name>
<dependencies>
<!-- Mybatis Plus:简化Mybatis开发 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- Lombok:通过注解简化开发 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
然后,应该在straw-api-user
的pom.xml
中添加依赖straw-commons
项目!先在父项目straw
中添加对straw-commons
的管理:
然后,在straw-api-user
的pom.xml
中添加依赖:
由于straw-api-user
通过以上添加依赖就可以使用straw-commons
中的User
类等相关实体类,则在straw-api-user
项目应该将model
包删除:
一旦删除,在straw-api-user
项目中原本导入User
、ClassInfo
的相关类和接口都会报错,需要将原本错误的import
语句删除并重新导入!
然后,在resources\mapper
下的各XML文件中的<resultmap>
节点也配置了实体类的全名,也需要修改包名部分!
全部完成后,点击Build
菜单 中的Rebuild
选项,会编译当前项目,是不会出现错误的!
3.查询某用户的信息
在straw-gateway
的pom.xml
中,也添加依赖straw-commons
:
在使用Spring Security验证登录时,会要求“根据用户名获取用户信息”,所以,在straw-gateway
中需要实现该功能,先添加持久层接口与XML文件,这些文件从straw-api-user
项目中复制过来,然后:
- 将
UserMapper.java
接口中的findByPhone()
方法删除; - 将
UserMapper.xml
中配置findByPhone()
的查询节点删除; - 修改
UserMapper.xml
中根节点的UserMapper
接口的名包;
然后,在启动类的声明之前配置持久层接口的包:
全部完成后,自行创建单元测试所需的文件夹和测试类文件,编写并执行单元测试:
4.根据用户名查询用户的权限信息
由于Spring Security在处理登录时,还会对成功登录的用户进行授权,所以,还需要实现“根据用户名查询该用户的权限的列表”查询功能,需要执行的SQL语句大致是:
SELECT DISTINCT(permission.id), permission.authority, permission.description
FROM permission
LEFT JOIN role_permission ON permission.id = role_permission.permission_id
LEFT JOIN role ON role_permission.role_id = role.id
LEFT JOIN user_role ON role.id = user_role.role_id
LEFT JOIN user ON user_role.user_id = user.id
WHERE user.username='13800138001'
ORDER BY permission.id;
要实现以上查询功能,首先,得准备一个类,用于封装此次的查询结果,则在cn.tedu.straw.gateway.vo
包中创建PermissionVO
类:
package cn.tedu.straw.gateway.vo;
import lombok.Data;
import java.io.Serializable;
@Data
public class PermissionVO implements Serializable {
private Integer id;
private String authority;
private String description;
}
一般,在项目中,每张表都有对应的实体类,常用于增、改操作,而查询不一定使用实体类,因为实体类与数据表完全对应,如果数据表中有20个字段,则实体类中应该有20个属性,而查询可能并不需要将所有字段的值都查出来,大多数数据表中都存在一些为了管理数据而存在的字段,在常规查询中根本不会使用到!所以,在查询时,通常会与查询功能对应的VO(Value Object)类。
然后,在cn.tedu.straw.gateway.mapper
包下创建PermissionMapper
接口,用于定义关于“权限”数据的增删改查方法:
package cn.tedu.straw.gateway.mapper;
import cn.tedu.straw.gateway.vo.PermissionVO;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface PermissionMapper {
/**
* 查询某用户的权限列表
*
* @param username 用户名
* @return 该用户的权限列表
*/
List<PermissionVO> findByUsername(String username);
}
然后,在resources/mapper
下创建(也可以是复制粘贴得到)PermissionMapper.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.gateway.mapper.PermissionMapper">
<select id="findByUsername" resultType="cn.tedu.straw.gateway.vo.PermissionVO">
SELECT DISTINCT(permission.id), permission.authority, permission.description
FROM permission
LEFT JOIN role_permission ON permission.id = role_permission.permission_id
LEFT JOIN role ON role_permission.role_id = role.id
LEFT JOIN user_role ON role.id = user_role.role_id
LEFT JOIN user ON user_role.user_id = user.id
WHERE user.username=#{username}
ORDER BY permission.id
</select>
</mapper>
完成后,在test
的cn.tedu.straw.gateway.mapper
下创建PermissionMapperTests
测试类,编写并执行单元测试:
package cn.tedu.straw.gateway.mapper;
import cn.tedu.straw.gateway.vo.PermissionVO;
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
public class PermissionMapperTests {
@Autowired
PermissionMapper mapper;
@Test
void findByUsername() {
String username = "13800138001";
List<PermissionVO> permissions = mapper.findByUsername(username);
System.err.println("权限数量:" + permissions.size());
for (PermissionVO permission : permissions) {
System.err.println("权限:" + permission);
}
}
}
5.在业务层实现查询用户详情及权限列表
在项目开发时,应该保证“除了Service以外,任何组件不得直接访问持久层”,具体表现为“只有在Service中才可以调用Mapper,其它类中不允许直接调用Mapper”。
所以,在实现Spring Security获取某用户的信息之前,应该先开发以上数据访问的业务层功能,后续,Spring Security将调用业务层对象实现数据访问,不会由Spring Security直接调用持久层!
先在cn.tedu.straw.gateway.service
包中创建IUserService
接口,并在接口中定义抽象方法:
package cn.tedu.straw.gateway.service;
import cn.tedu.straw.commons.model.User;
public interface IUserService {
/**
* 获取用户的详细信息
*
* @param username 用户名
* @return 该用户的详细信息,如果用户名未注册,则返回null
*/
User getUserInfo(String username);
}
然后,在cn.tedu.straw.gateway.service.impl
包中创建UserServiceImpl
实现类,实现以上接口,并在类的声明之前添加@Service
注解,重写抽象方法:
package cn.tedu.straw.gateway.service.impl;
import cn.tedu.straw.commons.model.User;
import cn.tedu.straw.gateway.mapper.UserMapper;
import cn.tedu.straw.gateway.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements IUserService {
@Autowired
UserMapper userMapper;
@Override
public User getUserInfo(String username) {
return userMapper.findByUsername(username);
}
}
完成后,在test
的cn.tedu.straw.gateway.service
包下创建UserServiceTests
测试类,编写并执行单元测试:
package cn.tedu.straw.gateway.service;
import cn.tedu.straw.commons.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class UserServiceTests {
@Autowired
IUserService service;
@Test
void getUserInfo() {
String username = "13800138001";
User user = service.getUserInfo(username);
System.err.println("查询结果:" + user);
}
}
在业务层,除了要查询用户详情以外,还需要查询该用户的权限,所以,在cn.tedu.straw.gateway.service
包中创建IPermissionService
接口,在接口中定义抽象方法:
package cn.tedu.straw.gateway.service;
import cn.tedu.straw.gateway.vo.PermissionVO;
import java.util.List;
public interface IPermissionService {
/**
* 查询某用户的权限列表
*
* @param username 用户名
* @return 该用户的权限列表
*/
List<PermissionVO> getUserPermissions(String username);
}
然后,在cn.tedu.straw.gateway.service.impl
包下创建PermissionServiceImpl
类,实现以上IPermissionService
接口,在类的声明之前添加@Service
注解,并实现以上抽象方法:
package cn.tedu.straw.gateway.service.impl;
import cn.tedu.straw.gateway.mapper.PermissionMapper;
import cn.tedu.straw.gateway.service.IPermissionService;
import cn.tedu.straw.gateway.vo.PermissionVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class PermissionServiceImpl implements IPermissionService {
@Autowired
PermissionMapper permissionMapper;
@Override
public List<PermissionVO> getUserPermissions(String username) {
return permissionMapper.findByUsername(username);
}
}
完成后,在test
的cn.tedu.straw.gateway.service
包下创建PermissionServiceTests
测试类,编写并执行单元测试:
6.通过用户名获取用户基本信息和权限列表
在Spring Security验证用户登录时,将提供尝试登录的用户名(用户在登录页面输入的用户名),并要求获得UserDetails
接口类型的对象,该对象中应该包含用户的基本信息及权限列表,剩下的就由Spring Security完成验证,如果验证通过,Spring Security会将用户的信息存储在Session中,并在后续的访问过程中自动判断用户是否已经登录,及检查相关的授权。
但是,在实际使用时,我们可能需要使用到更多的用户属性,在UserDetails
中是没有定义的,例如用户的Id、昵称等数据!所以,应该自定义类进行扩展Spring Security的User
类型(该类型是实现 了UserDetails
接口的)!
由于登录的用户信息在多个不同的子模块项目中都需要使用到,例如在straw-gateway
中,在验证登录时应该返回用户信息,在straw-api-user
或其它子模块项目中,需要获取登录的用户信息来验证用户身份或存储数据等!
所以,先在straw-commons
的pom.xml
中添加依赖Spring Security框架,否则,就不识别User
类,就无法扩展:
然后,在straw-commons
的cn.tedu.straw.commons.security
包中自定义LoginUserInfo
类,扩展自User
类:
package cn.tedu.straw.commons.security;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
@Setter
@Getter
public class LoginUserInfo extends User {
private Integer id;
private String nickname;
public LoginUserInfo(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public LoginUserInfo(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
@Override
public String toString() {
return "LoginUserInfo >>> " +
"id=" + id +
", nickname=" + nickname +
',' + super.toString();
}
}
接下来,在straw-gateway
实现UserDetailsService
接口,重写接口中的方法,返回UserDetails
类型的对象:
package cn.tedu.straw.gateway.security;
import cn.tedu.straw.commons.model.User;
import cn.tedu.straw.commons.security.LoginUserInfo;
import cn.tedu.straw.gateway.service.IPermissionService;
import cn.tedu.straw.gateway.service.IUserService;
import cn.tedu.straw.gateway.vo.PermissionVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
IUserService userService;
@Autowired
IPermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询用户信息
User user = userService.getUserInfo(username);
// 如果没有查询到用户信息,则返回null
if (user == null) {
return null;
}
// 根据用户名查询该用户的权限信息(列表)
List<PermissionVO> permissions = permissionService.getUserPermissions(username);
// 基于以上权限列表创建GrantedAuthority集合
List<GrantedAuthority> authorities = new ArrayList<>();
for (PermissionVO permission : permissions) {
authorities.add(new SimpleGrantedAuthority(permission.getAuthority()));
}
// 创建返回对象
// UserDetails userDetails = org.springframework.security.core.userdetails.User.builder()
// .username(user.getUsername())
// .password(user.getPassword())
// .disabled(user.getIsEnabled() == 0)
// .accountLocked(user.getIsLocked() == 1)
// .accountExpired(false)
// .credentialsExpired(false)
// .authorities(authorities)
// .build();
// 关于构造方法的参数,在构造方法的括号中按下Ctrl+P可以提示参数列表
// String username:用户名
// String password:密码
// boolean enabled:账号是否启用
// boolean accountNonExpired:账号是否未过期
// boolean credentialsNonExpired:证书是否未过期
// boolean accountNonLocked:账号未被锁定
// Collection<? extends GrantedAuthority> authorities:权限列表
LoginUserInfo loginUserInfo = new LoginUserInfo(
user.getUsername(),
user.getPassword(),
user.getIsEnabled() == 1,
true,
true,
user.getIsLocked() == 0,
authorities
);
loginUserInfo.setId(user.getId());
loginUserInfo.setNickname(user.getNickname());
// 返回用户信息
return loginUserInfo;
}
}
完成后,在test
的cn.tedu.straw.gateway.security
包中创建UserDetailsTests
测试类,测试获取用户信息:
package cn.tedu.straw.gateway.security;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.userdetails.UserDetails;
@SpringBootTest
public class UserDetailsTests {
@Autowired
UserDetailsServiceImpl userDetailsService;
@Test
void loadUserByUsername() {
String username = "13800138001";
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
System.err.println("根据用户名" + username + "查询到的用户信息:");
System.err.println(">>> " + userDetails);
}
}
当提供的用户名是正确的时,测试结果例如:
根据用户名13800138001查询到的用户信息:
>>> LoginUserInfo >>>
id=1,
nickname=WKJ,
cn.tedu.straw.commons.security.LoginUserInfo@5549e271:
Username: 13800138001;
Password: [PROTECTED];
Enabled: true;
AccountNonExpired: true;
credentialsNonExpired: true;
AccountNonLocked: true;
Granted Authorities:
/answer/post,
/comment/post,
/question/detail,
/question/post,
/question/upload,
/web/index