- 什么是Spring安全框架
- 为什么需要Spring-Security
- 启动Spring-Security
- 访问控制器方法
- 密码加密
- Spring-Security的权限管理功能
- 实现数据库中的用户登录
- 设置放行页面
- 自定义登录页面
- 注册功能流程分析
- 注册业务流程
- 注册业务准备
- 业务逻辑层概述
- 开发注册业务逻辑层代码
- 开发控制层代码
- 修改为异步测试
- Spring验证框架
- 表单验证基本概念
- 什么是Spring验证框架
- Spring Validation的使用
- 随笔
Spring安全:Spring-Security
是Spring提供的安全管理框架,功能是提供一个安全可靠的登录功能,并且支持权限管理功能,而且自带判断当前用户是否登录的过滤器,如果用户没有登录会跳转到登录页面
为什么需要Spring-Security使用Spring-Security框架能够使新手程序也能写出企业级别安全的登录功能
Spring-Security包含了权限管理的功能,能够方便的保存一个用户的各种权限,使用简单地方式判断这个用户是否包含这些权限,决定是否允许访问
能够帮助程序员提升编写登录和权限管理功能的开发效率
启动Spring-Security启动Spring-Security非常的简单
只需要添加依赖即可
org.springframework.boot spring-boot-starter-security
加好这个依赖Spring-Security这个框架就会在项目中生效了
现在所有项目中的资源都会被Spring-Security保护
也就是说默认情况下,要想访问当前项目的任何资源,都需要先登录。
而登录方法是:
用户名:user
密码:启动服务时idea控制台出现的随机密码
打开创建好的UserController在其中添加一个方法
代码如下:
@RestController
//在类上编写@RequestMapping注解,表示当前控制器中的方法都需要以本注解
//添加的路径前缀来访问
@RequestMapping("/v1/users")
public class UserController {
//编写控制器方法
//结合类上面的注解,访问笨方法的最终路径是
//localhost:8080/v1/users/get
@GetMapping("/get")
public String get(){
return "Hello html";
}
}
重启服务,访问localhost:8080/v1/users/get
也是需要登录的,因为控制器的响应也属于网站资源,受到Spring-Security保护
上面我们登录只能使用user这个用户,而且密码每次都要到控制台复制,比较麻烦
Spring-Security允许我们自定义的用户名和密码配置到application.properties中配置
#配置Spring-Security的自定义用户名和密码 spring.security.user.name=admin spring.security.user.password=123456
但是这样配置的话,任何可以看到配置文件的人都可以登陆这个网站
所以我们需要学习密码加密,加密之后即使别人看到密码,也不能登录
我们可以使用市面上流星的安全加密算法:bcrypt
这个加密算法可以将任何数据进行加密保存,保证安全
在测试类中进行一个加密操作,代码如下:
@SpringBootTest
public class PasswordTest {
//对Bcrypt加密对象实例化
PasswordEncoder encoder = new BCryptPasswordEncoder();
//执行加密测试
@Test
public void test(){
//利用加密对象将str字符串加密为pwd
String str = "123456";
String pwd = encoder.encode(str);
System.out.println(pwd);
}
}
运行输出了一个加密结果后发现每次结果都不同,因为每次加密秘结果相同的话安全性较低,bcrypt加密算法采用了"随机校验"技术,让每次生成结果都不同.
加密结果
加密完成下面进行验证的代码,bcrypt提供了验证的方法,可以判断一个字符串是否匹配一个加密结果
// 执行验证测试
@Test
public void match(){
// 下面的方法验证一个字符串是否匹配一个加密结果
// 返回boolean类型
boolean b=encoder.matches("123456",
"$2a$10$B5Ba4G77NuxAcRJ/iucipOaXjc/3uranz.lMW008IVxRdG
BATv8d2");
System.out.println("匹配结果:"+b);
}
最终目的是将加密结果配置在配置文件中application.properties文件修改为
#配置Spring-Security的自定义用户名和密码
spring.security.user.name=admin
spring.security.user.password={bcrypt}$2a$10$.6XmtLGrTwxO/JWWJCoc4OjoBQ7RG6cZ1WEHtYNbbYQWzVaqjTj2i
Spring-Security的权限管理功能
我们最终的登录是要支持现有数据库中所有user的数据
现在只能支持配置文件中的用户
如果要实现数据库登录,首先要有机会在java代码中设置用户名密码
创建一个security包,包中创建SecurityConfig,代码如下:
@Configuration
//启用spring-security提供的权限管理功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//当前类继承WebSecurityConfigurerAdapter
//能够重写这个父类中的方法,这个父类中的方法都是用于设置权限管理的
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("tom")
.password("{bcrpt}$2a$10$.6XmtLGrTwxO/JWWJCoc4OjoBQ7RG6cZ1WEHtYNbbYQWzVaqjTj2i")
.authorities("test");
//上面代码的含义是在Spring-Security框架中定义了一个用户
//用户名是tom,密码是123456
//具有"test"这个资格可以使当前用户具有访问test资格资源的访问权限
//当我们设置这个用户之后,配置文件中设置的用户admin就失效了
}
}
控制器方法可以设定当前方法需要什么特殊权限才能访问,如果不设置默认情况下登录就可以访问
修改UserController代码如下:
@RestController
// 在类上编写下面注解,表示当前控制器中的方法都需要
// 以本注解添加的路径前缀来访问
@RequestMapping("/v1/users")
public class UserController {
// 编写控制器方法
// 结合类上面的注解,访问本方法的最终路径是
// localhost:8080/v1/users/get
@GetMapping("/get")
public String get(){
return "Hello html";
}
// 上面的方法没有设置特殊权限登录就可以访问
// 下面方法设置特殊权限,必须有匹配的资格才能访问
@GetMapping("/list")
// 当前这个方法必须是拥有test资格的用户才能访问
@PreAuthorize("hasAuthority('run')")
public String list(){
return "get list";
}
}
实现数据库中的用户登录
通过之前的部分,现在知道在Java代码中,要想让用户登录至少要提供用户名,密码,和当前用户权限
数据库用户表中没有直接提供当前用户的权限,那么我们就要根据对当前用户的id查询当前用户的权限
这个查询可能涉及上面的5张表
因为用户对角色和角色对权限都是多对多
今后面试时如果问到权限数据的实现方式,需要回答上面的5张表
我们需要编写一个根据用户id查询所有权限的5表联查的sql语句
SELECt p.id , p.name FROM user u LEFT JOIN user_role ur ON u.id=ur.user_id LEFT JOIN role r ON r.id=ur.role_id LEFT JOIN role_permission rp ON r.id=rp.role_id LEFT JOIN permission p ON p.id=rp.permission_id WHERe u.id=11
我们需要在数据访问层编写这个方法,在登录业务中需要时调用
打开UserMapper编写代码如下
@Repository public interface UserMapper extends baseMapper{ //根据用户id 查询用户所有权限的方法 @Select("SELECT p.id , p.namen" + "FROM user un" + "LEFT JOIN user_role ur ON u.id=ur.user_idn" + "LEFT JOIN role r ON r.id=ur.role_idn" + "LEFT JOIN role_permission rp ON r.id=rp.role_idn" + "LEFT JOIN permission p ON p.id=rp.permission_idn" + "WHERe u.id=#{id}") List findUserPermissionsById(Integer id); //根据用户名查询用户对象 @Select("select * from user where username=#{username}") User findUserByUsername(String username); }
有了用户名,密码和用户权限等信息
下面就可以按照Spring-Security规定方式进行登录代码的编写了
我们需要自己编写一个类,这个类实现Spring-Security提供的恶一个接口UserDetailsService,而这个接口中需要实现一个方法,这个方法的功能是根据用户输入在登录框中的用户名进行用户信息(用户名密码权限)的查询,返回值必须是UserDetails,我们需要将这个类型对象实例化后赋值最后返回以完成登录
在service.impl包中新建一个类UserDetailsServiceImpl
代码如下
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
//当前类需要保存到Spring容器@Component不能少
//需要基于Spring-Security设计的方法进行登录,实现UserDetailsService接口
//下面方法是接口提供的,我们来实现
//方法的参数是用户在登录表单编写的用户名
//方法的返回值是UserDetails类型对象包含登录需要的用户名密码权限等
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.根据用户名查询用户对象
User user= userMapper.findUserByUsername(username);
//2.判断是否能够查询到用户,没有该用户表示用户名不存在
if (user==null){
return null;
}
//3.根据用户id查询用户的所有权限
List permissions=
userMapper.findUserPermissionsById(user.getId());
//4.将权限的集合转换为String类型数组进行赋值
String[] auth = new String[permissions.size()];
int i =0;
for (Permission p : permissions){
auth[i]=p.getName();
i++;
}
//5.构建UserDetails对象
UserDetails details =
org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(auth).accountLocked(user.getLocked()==1)//设置当前用户是否锁定
.disabled(user.getEnabled()==0)//设置当前用户是否可用 false表示可用
.build();
//6.返回
return details;
}
}
上面的代码是Spring-Security要求我们编写的完成登录功能的代码
我们要想登录成功,还要将这个类型对象和Spring-Security建立关系
回到security包中的SecurityConfig类
将我们之前编写的configure方法修改为
// 表示当前配置类是配置Spring框架的
@Configuration
// 启动Spring-Security提供的权限管理功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends
WebSecurityConfigurerAdapter {
//当前类继承WebSecurityConfigurerAdapter
// 能够重写这个父类中的方法,这个父类中的方法都是用于设置权限管理的
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
设置放行页面
当今流行的网站都是有些页面允许不登录就能访问
而我们现在的Spring-Security下所有资源都需要登录才能访问
我们如果想放行一些页面需要配置下面代码
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 设置网站的访问及放行规则
// 下面的方法开始指定路径
.antMatchers(
"/index_student.html",
"/css/*",
"/js/*",
"/img/**",
"/bower_components/**")
.permitAll() // 上面的路径是全部允许的(不需要登录就能访问)
.anyRequest() // 除上面之外的其他路径
.authenticated() // 需要登录才能访问
.and() //上面的配置完成了,开始配置下面的
.formLogin(); // 使用表单进行登录
}
自定义登录页面
Spring-Security提供的默认登录页面不能体现当前网站的特征,也不能编写其他功能或连接,很受限制,我们希望能够使用自定义的login.html页面进行登录操作
也是需要进行对应的配置
SecurityConfig继续配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //禁用防跨域攻击功能
.authorizeRequests() // 设置网站的访问及放行规则
// 下面的方法开始指定路径
.antMatchers(
"/index_student.html",
"/css/*",
"/js/*",
"/img/**",
"/bower_components/**",
"/login.html")
.permitAll() // 上面的路径是全部允许的(不需要登录就能访问)
.anyRequest() // 除上面之外的其他路径
.authenticated() // 需要登录才能访问
.and() //上面的配置完成了,开始配置下面的
.formLogin() // 使用表单进行登录
.loginPage("/login.html") //配置登录时显示的页面
.loginProcessingUrl("/login") //配置处理登录的路径
.failureUrl("/login.html?error")// 登录失败跳转的页面
.defaultSuccessUrl("/index_student.html")// 登录成功跳转的页面
.and()
.logout()
.logoutUrl("/logout") // 配置登出的链接
.logoutSuccessUrl("/login.html?logout");// 登出后跳转回登录页
}
注册功能流程分析
注册业务流程
- 学生填写注册表单信息
- 提交注册信息到控制器
- 控制器接收到信息调佣业务逻辑层方法
- 业务逻辑层中判断邀请码,手机号并对密码加密后进行数据库新增
- mapper层执行新增方法,返回到业务逻辑层
- 业务逻辑层将注册结果返回给控制鞥
- 控制层将最终信息显示在页面上
首设置注册页面和控制器路径的放行
打开SecurityConfig配置类,进行放行配置
http.csrf().disable() //禁用防跨域攻击功能
.authorizeRequests() // 设置网站的访问及放行规则
// 下面的方法开始指定路径
.antMatchers(
"/index_student.html",
"/css/*",
"/js/*",
"/img/**",
"/bower_components/**",
"/login.html",
"/register.html",
"/register")
.permitAll() // 上面的路径是全部允许的(不需要登录就能访问)
.anyRequest() // 除上面之外的其他路径
.authenticated() // 需要登录才能访问
.and() //上面的配置完成了,开始配置下面的
.formLogin() // 使用表单进行登录
.loginPage("/login.html") //配置登录时显示的页面
.loginProcessingUrl("/login") //配置处理登录的路径
.failureUrl("/login.html?error")// 登录失败跳转的页面
.defaultSuccessUrl("/index_student.html")// 登录成功跳转的页面
.and()
.logout()
.logoutUrl("/logout") // 配置登出的链接
.logoutSuccessUrl("/login.html?logout");// 登出后跳转回登录页
根据表单参数创建vo类
@Data
public class RegisterVo implements Serializable {
private String inviteCode; //邀请码
private String phone; //手机号用户名
private String nickname; //昵称
private String password; //密码
private String /confirm/i; //确认密码
}
还需要自定义异常类
在我们编写的业务发生异常不能继续运行时,使用抛出异常的方式反馈
错误信息
我们定义一个自定义异常类,ServiceException
来表示业务逻辑运行过程中发生的各种不能继续运行程序的异常
例如:邀请码不正确手机号已经被注册
新建一个包exception,新建类代码如下
public class ServiceException extends RuntimeException{
private int code = 500;
public ServiceException() { }
public ServiceException(String message) {
super(message);
}
public ServiceException(String message, Throwable
cause) {
super(message, cause);
}
public ServiceException(Throwable cause) {
super(cause);
}
public ServiceException(String message, Throwable
cause,
boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression,
writableStackTrace);
}
public ServiceException(int code) {
this.code = code;
}
public ServiceException(String message, int code) {
super(message);
this.code = code;
}
public ServiceException(String message, Throwable
cause,
int code) {
super(message, cause);
this.code = code;
}
public ServiceException(Throwable cause, int code)
{
super(cause);
this.code = code;
}
public ServiceException(String message, Throwable
cause,
boolean enableSuppression,
boolean writableStackTrace, int code) {
super(message, cause, enableSuppression,
writableStackTrace);
this.code = code;
}
public int getCode() {
return code;
}
}
还可以设置简单条件,适合偶尔一次查询数据库使用
我们在测试类中编写一个测试,按邀请码查询班级信息
代码如下
// 根据邀请码查询班级信息
// 如果写sql语句:
// select * from classroom where invite_code='JSD2001-
706246'
// 如果使用QueryWrapper进行查询 代码如下
@Autowired
ClassroomMapper classroomMapper;
@Test
public void query(){
// 我们实例化一个QueryWrapper的对象
// 这个对象其实就是代表查询的条件,泛型是实体类的类型
QueryWrapper query=new QueryWrapper<>();
// 设置查询条件 query.eq([列名],[值])
query.eq("invite_code","JSD2001-70624");
// 按QueryWrapper对象设置好的条件进行查询的操作
// selectOne方法只支持最多返回1行数据,否则报错,返回值是实
体类型
Classroom
classroom=classroomMapper.selectOne(query);
System.out.println(classroom);
}
业务逻辑层概述
Vrd项目中完成一次请求响应流程一般会由两个部分组成
请求->控制器(controller)->数据访问层(mapper)
上面的执行流程只能处理相对简单的业务逻辑层
如果遇到企业中的相对复杂的业务逻辑就不能很好的处理了
每个类都应该有自己的职责
// 根据邀请码查询班级信息
// 如果写sql语句:
// select * from classroom where invite_code=‘JSD2001-
706246’
// 如果使用QueryWrapper进行查询 代码如下
@Autowired
ClassroomMapper classroomMapper;
@Test
public void query(){
// 我们实例化一个QueryWrapper的对象
// 这个对象其实就是代表查询的条件,泛型是实体类的类型
QueryWrapper query=new QueryWrapper<>();
// 设置查询条件 query.eq([列名],[值])
query.eq(“invite_code”,“JSD2001-70624”);
// 按QueryWrapper对象设置好的条件进行查询的操作
// selectOne方法只支持最多返回1行数据,否则报错,返回值是实
体类型
Classroom
classroom=classroomMapper.selectOne(query);
System.out.println(classroom);
}
controller:职责就是接收前端页面的信息和将结果响应给页面,其他的事
情尽量不管
mapper:完成对数据库的增删改查操作,其他的操作也不管
如果出现了既不属于controller的也不属于mapper职责的工作,就需要
写在业务逻辑层中
service(业务逻辑层):职责就是将前端发送来的信息经过处理再调用数据
访问层的功能,例如我们接收了用户输入的邀请码但是需要判断是否正
确
企业标准中,service又由两个部分组成
service和service.impl
service中保存业务逻辑层接口:一般命名为IXXXService(开头的I表示
Interface)
service.impl中保存业务逻辑层实现类:一般命名为
XXXServiceImpl(Impl表示实现的缩写)
之所以采用接口配实现类的形式,是为了解耦
所以在需要业务逻辑层代码时,我们都声明接口类型
再今后我们开发程序的模型中,控制层,业务逻辑层,数据访问层这三层结
构如下
实际开发中,应该先完成数据访问层的编写,但是当前业务中,所有数据库
操作都是基本增删改查,已经由MybatisPlus提供了,所以Mapper层不需
要编写代码
先编写业务逻辑层接口
IUserService添加方法如下
public interface IUserService extends IService{ // 在接口中如果想转到实现类快捷键Ctrl+Alt+B void registerStudent(RegisterVo registerVo); }
UserServiceImpl实现类代码如下
@Service public class UserServiceImpl extends ServiceImplimplements IUserService { //注入注册需要的各种依赖 @Autowired private UserMapper userMapper; @Autowired private ClassroomMapper classroomMapper; @Autowired private UserRoleMapper userRoleMapper; @Override public void registerStudent(RegisterVo registerVo) { // 1.根据用户输入的邀请码获得班级信息 QueryWrapper query=new QueryWrapper<>(); query.eq("invite_code",registerVo.getInviteCode()); Classroom classroom=classroomMapper.selectOne(query); // 2.判断班级信息是否存在,不存在直接抛异常 if(classroom==null){ throw new ServiceException("邀请码错误!"); } // 3.根据用户输入的手机号,获得用户信息 User user=userMapper.findUserByUsername( registerVo.getPhone()); // 4.如果能够获得用户信息,表示当前手机号已经被注册,抛出 异常 if(user!=null){ throw new ServiceException("手机号已经被注 册!"); } // 5.对用户输入的密码进行加密 PasswordEncoder encoder=new BCryptPasswordEncoder(); String pwd=" {bcrypt}"+encoder.encode(registerVo.getPassword()); // 6.实例化用户对象,为各个属性赋值,收集用户信息 User u=new User() .setUsername(registerVo.getPhone()) .setNickname(registerVo.getNickname()) .setPassword(pwd) .setClassroomId(classroom.getId()) .setCreatetime(LocalDateTime.now()) .setEnabled(1) .setLocked(0) .setType(0); // 7.执行新增用户对象的操作 int num=userMapper.insert(u); if(num!=1){ throw new ServiceException("数据库忙"); } // 8.执行新增用户角色关系表的操作 UserRole userRole=new UserRole() .setUserId(u.getId()) .setRoleId(2); num=userRoleMapper.insert(userRole); if(num!=1){ throw new ServiceException("数据库忙"); } } }
推荐大家编写完比价复杂的业务逻辑代码时进行测试
代码如下
@Autowired
IUserService userService;
@Test
public void add(){
RegisterVo registerVo=new RegisterVo();
registerVo.setPhone("13033012345");
registerVo.setNickname("大龙");
registerVo.setInviteCode("JSD2001-706246");
registerVo.setPassword("123456");
userService.registerStudent(registerVo);
System.out.println("ok");
}
开发控制层代码
我们先来编写控制层接收表单信息的代码
创建SystemController类
编写代码如下
@RestController
// lombok提供的一个记录日志用的注解
// 一旦在类上添加@Slf4j,这个类的方法中就可以使用log对象记录日志
@Slf4j
public class SystemController {
@Autowired
private IUserService userService;
@PostMapping("/register")
public String register(RegisterVo registerVo){
//利用日志对象,将接收到的信息输出到控制台
log.debug("接收到用户信息:{}",registerVo);
try {
userService.registerStudent(registerVo);
return "ok";
}catch (ServiceException e){
log.error("注册失败",e);
return e.getMessage();
}
}
}
重启服务测试
注册成功表示代码正确,检查数据库user表和user_role表示的信息
修改为异步测试gitee同步更新中
将一个同步的注册修改为异步注册需要如下修改
- 页面中需要的支持(vue,axios等)
- 编写并引用js代码


