续 Spring 安全框架
密码加密的实现
上次课讲解了密码加密的概念
介绍了bcrypt密码加密的算法
下面我们通过一个测试,演示这个算法
// 声明密码加密操作对象
PasswordEncoder encoder=new BCryptPasswordEncoder();
// 加密测试
@Test
public void pwd(){
String str="123456";
// 执行加密,使用encode方法参数是要加密的字符串,返回值是加密结果
String pwd= encoder.encode(str);
System.out.println(pwd);
//$2a$10$kVuKchYUN92TGcQb./H.iOHCT8LOIho12bI1OTb5xPTassynjdsES
}
运行测试方法,能够得到加密结果
我们发现每次加密结果是不同的
因为如果每次加密结果时固定的,反而容易被人破解,并不安全
bcrypt加密算法采用了"随机加盐"技术,每次加密结果不同,提高了安全等级
下面我们来测试,加密得到的字符串,能不能正确的验证成功
"验证"指判断当前加密字符串是不是指定字符串生成的
// 验证操作
@Test
public void match(){
// 验证一个字符串是否能够加密为指定的加密结果
// 方法matches([原字符串],[加密字符串])返回值是boolean类型
// 返回值为真表示验证通过,返回值为假表示验证失败
boolean b=encoder.matches("123456",
"$2a$10$kVuKchYUN92TGcQb./H.iOHCT8LOIho12bI1OTb5xPTassynjdsE");
System.out.println("验证结果为:"+b);
}
上面的测试如果运行输出验证通过(true),表示字符串和加密结果匹配
否则会输出false
下面将我们的application.properties配置文件中的用户密码设置为加密的
# Spring-Security自定义登录用户名和密码的配置
spring.security.user.name=admin
spring.security.user.password={bcrypt}$2a$10$kVuKchYUN92TGcQb./H.iOHCT8LOIho12bI1OTb5xPTassynjdsES
{bcrypt}是"算法id"的声明定义
表示后面的字符串是由bcrypt算法加密得来的
登录时会按照相同的算法来验证
Spring-Security配置登录与权限管理
上面章节中我们将登录用户信息保存在application配置文件中
实际登录时,一定是从数据库来获得用户信息的,所以登录一定会编写在java代码中
我们学习如何在java配置类中配置一个用户的登录,顺便演示用户的权限管理功能
portal项目中创建一个security包
这个包中创建SecurityConfig类,进行配置代码如下
// SpringBoot启动时扫描到@Configuration标记的类,会自动加载其中的配置
// 所有SpringBoot框架环境下@Configuration必须写才能配置生效
@Configuration
// 启动Spring-Security配置的注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// WebSecurityConfigurerAdapter是我们需要基础的父类
// 这个父类提供了配置Spring-Security运行的基本方法
// 我们要想修改配置的话,重写它的方法即可
// 下面我们就配置一个可以登录的用户
// 一旦重写这个方法,配置文件中的admin就失效了
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 删除原有的super调用
// 在代码中配置一个等于用户
// inMemory表示在内存中保存一个用户
auth.inMemoryAuthentication()
.withUser("tom")
.password("{bcrypt}$2a$10$ELGiEhKyLlO9r3.WVOkHDe16JTCKCErcABhElD5CF7ZwQ.Hm6sVRW")
.authorities("answer");
// 我们配置了一个可以登录的用户
// 用户名tom 密码 888888
// 在代码最后一行,我们授予了当前tom用户answer的授权\资格
// 这个授权或资格可以访问需要对应授权的控制器方法
}
}
重启服务
登录tom
登录成功后可以访问项目的大部分资源
但是如果有控制器方法需要特殊授权才能执行
运行前Spring-Security会验证当前用户是否具有这个资格
我们在UserController中编写两个需要不同授权才能访问的方法代码如下
@GetMapping("/answer")
// 下面的注解规定当前用户必须包含answer授权
// 才能访问下面的控制方法
@PreAuthorize("hasAuthority('answer')")
public String answer(){
return "允许回答问题!";
}
@GetMapping("/delete")
@PreAuthorize("hasAuthority('delete')")
public String delete(){
return "允许删除回答!";
}
重启服务
tom用户具有answer授权
所以可以访问/answer方法
但是访问/delete方法时返回403错误,表示没有权限访问
这样就能显示某些用户访问某些资源
如果想让一个用户具有多个授权,可以改写登录配置如下
String[] as={"answer","delete"};
// inMemory表示在内存中保存一个用户
auth.inMemoryAuthentication()
.withUser("tom")
.password("{bcrypt}$2a$10$ELGiEhKyLlO9r3.WVOkHDe16JTCKCErcABhElD5CF7ZwQ.Hm6sVRW")
.authorities(as);
再重启服务,登录tom
就可以访问/answer和/delete两个方法了!
用户与权限
上面章节,我们学习了定义一个用户的登录,并且设置一个用户具有的授权
我们在登录当前数据库中用户时,又如何确定这个用户的授权呢?
现在登录需要的信息有
1.用户名
2.密码(加密的)
3.用户的所有权限
上面的信息都保存在数据库中,用户名和密码保存在user表里
但是用户的授权(权限)是需要相对复杂的查询才能查询出来的
上的5张表表示的是用户\角色\权限的关系
我们需要根据用户的id查询出这个用户具有哪些角色
在根据用户的角色查询出它的所有权限
他们之前的关系表有user_role和role_permission
所有这个查询是一个需要连接5张表的sql查询语句
我们需要编写这个sql语句如下
-- 根据用户id进行5表连查获得权限信息
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
上面的sql语句运行能够成功的话
要编写到我们的UserMapper中来执行
@Repository
public interface UserMapper extends BaseMapper<User> {
// 根据用户id查询用户所有权限的方法
@Select("SELECT p.id , p.name\n" +
"FROM user u\n" +
"LEFT JOIN user_role ur ON u.id=ur.user_id\n" +
"LEFT JOIN role r ON r.id=ur.role_id\n" +
"LEFT JOIN role_permission rp\n" +
"ON r.id=rp.role_id\n" +
"LEFT JOIN permission p ON p.id=rp.permission_id\n" +
"WHERE u.id=#{id}")
List<Permission> findUserPermissionsById(Integer id);
// 根据用户名查询用户信息
@Select("select * from user where username=#{username}")
User findUserByUsername(String username);
}
UserMapper编写完毕
推荐大家进行测试
测试类中编写测试代码如下
@Autowired
UserMapper userMapper;
@Test
public void userTest(){
// 先运行根据用户名获得用户对象的方法
// id:11 学生用户名为st2 id:3 讲师用户名为tc2
User user=userMapper.findUserByUsername("tc2");
// 根据用户对象中的id属性查询这个用户的所有权限
List<Permission> list=userMapper
.findUserPermissionsById(user.getId());
System.out.println(user);
for(Permission p: list){
System.out.println(p);
}
}
Spring-Security数据库用户登录
上面章节,我们已经创建了必要的登录配置类,以及响应的连接数据库的Mapper方法
下面我要就来完成数据库中用户的登录功能
Spring-Security框架已经设置要的登录流程中的关键步骤:
- 控制器的编写
- 用户名密码的获取
- 用户输入的密码和数据库中密码的对比验证
都由Spring-Security框架封装好了,不需要咱们编写
我们只需要完成Spring-Security框架提供的一个接口
实现这个接口的方法,具体来说就是根据用户输入的用户名,获得这个用户的相关信息,这些信息包括
- 用户名
- 密码
- 所有权限
- 其它信息…
我们现在需要做的就是编写一个类,实现这个接口,按要求编写接口中的方法并返回正确的返回值,最后通过一个配置实现和Spring-Security框架的关联,其它都由Spring-Security框架完成
我们打开当前项目的service包中的impl包
这个包里创建一个UserDetailsServiceImpl类
代码如下
// 当前类要保存到Spring容器中以便Spring-Security使用
// 这个类实现的接口UserDetailsService,是Spring-Security框架
// 提供的一个接口,用于我们实现数据库登录
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
// loadUserByUsername方法是UserDetailsService接口中声明的
// 方法的功能是根据用户输入的用户名返回这个用户的详情信息(详情\Details)
// 返回值UserDetails也是Spring-Security提供的,
// 使用Spring-Security框架识别的用户信息
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 方法的参数就是用户登录时输入的用户名
// 1. 根据用户名获得用户对象
User user=userMapper.findUserByUsername(username);
// 2. 验证用户对象是不是null
if(user==null){
// 如果是null,表示用户名不存在,返回null表示登录失败
return null;
}
// 3. 根据用户id查询所有权限
List<Permission> 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)
// 设置当前账号是否锁定,false表示锁定
.accountLocked(user.getLocked()==1)
// 设置当前账号是否可用,false表示可用
.disabled(user.getEnabled()==0)
.build();
// 6. 返回用户详情UserDetails对象
// 千万别忘了返回details
return details;
}
}
到此为止我们只是编写了一个类而已
没有和Spring-Security框架进行关联,我们编写的代码就不能真正生效
要想生效,需要经过SecurityConfig类中进行登录配置
我们将SecurityConfig类修改代码如下
// 下面的配置是让我们编写的UserDetailsServiceImpl类生效的
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 让Spring-Security框架进行登录操作时
// 调用我们编写的userDetailsService中的方法
auth.userDetailsService(userDetailsService);
}
重启服务
就能使用数据库中的用户来登录了
建议测试st2和tc2用户,密码都是888888
设置页面权限
一个网站,不应该所有页面都需要登录之后才能访问
我们如果想将学生首页设置为不需要登录就能访问的状态
可以通过修改配置文件来实现
SecurityConfig类添加新的configure方法实现代码如下
// 配置页面权限的方法
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 设置禁用防跨域攻击
.authorizeRequests() // 开始设置页面访问权限
.antMatchers(// 指定路径
"/index_student.html",
"/css/*",
"/js/*",
"/img/**",
"/bower_components/**"
).permitAll() // 上述路径全部放行(不登录就能访问)
.anyRequest() // 除此之外的其它请求
.authenticated() // 需要登录才能访问
.and() // 这是个分割,上面配置已经完毕下面编写新配置
.formLogin(); // 支持表单登录
}
重启服务
测试是否访问学生首页不需要登录
方法其它页面还需要登录
自定义登录页
Spring-Security框架提供的登录页功能单一
没有中文,没有个性化设置
我们希望能够使用自己的登录页面来替换掉
也是在上面的配置中继续编写即可
// 配置页面权限的方法
@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") // 配置登录页为login.html
.loginProcessingUrl("/login") // 配置表单提交登录信息的路径
.failureUrl("/login.html?error") // 配置登录失败跳转的路径
.defaultSuccessUrl("/index_student.html") //登录成功后跳转的页面*
.and() //登录设置完成,开始设置登出
.logout() // 开始设置登出
.logoutUrl("/logout") // 设置登出路径
.logoutSuccessUrl("/login.html?logout");//设置登出成功后跳转的页面
/*
defaultSuccessUrl设置的是登录成功时默认跳转的页面
特指用户没有指定要访问的页面时,登录成功时跳转的页面
如果用户指定的要跳转的页面,登录成功时优先访问用户指定的页面
*/
}
经过上面的配置
我们的项目支持了我们自己编写的登录页面
并且支持登出功能
实现注册功能
分析注册业务流程
注册业务流程描述
1.学生在注册页面填写表单信息
2.点击"注册"按钮将表单提交给Controller控制器
3.Controller控制器接收到表单信息之后,调用业务逻辑层(Service)方法
4.Service方法中判断邀请码是否正确,手机号是否已经被注册,密码加密等操作
5.Service中调用Mapper方法新增用户
6.最后由控制器返回新增结果到页面显示
英文
Enable:启用\启动
Global:全局\全球
Adapter:适配器
Configurer:配置
Authentication:授权
今后几乎所有auth开头的单词都是授权的意思,只是不同的词性
Memory:记忆\计算机中就是内存
role:角色
permission:权限
Details:详情
default:默认
学校中的用户和权限
用户 中间表 角色 中间表 权限
杨过 学生 签到
令狐冲 任课老师 成绩管理
欧阳锋 班主任 学生管理
乔峰 年级组长 课程管理
段誉 教导主任 教职工管理
洪七公 校长 学校倒闭
表与表之间的关系
1.一对一
2.一对多(多对一):
班级和学生
部门和员工
特征:在多(学生表)的一方,保存一个一的一方(班级表)的id列
一对多是所有关系中最常见的关系
3.多对多
传统义务教育阶段的老师和班级
一个老师给多个班级讲课
一个班级被多个老师教
特征:在两张表之间,创建一个关系表
关系表中保存班级和老师的关系
Spring-Security登录流程
登录流程本身并不是重点内容,重点内容时了解我们实现登录需要编写的代码
1.用户输入用户名密码点击提交
2.Spring-Security提供的控制器会接收登录请求获得用户名和密码
3.Spring-Security会调用我们编写的loadUserByUsername方法获得用户详情
4.Spring-Security获得用户详情之后会验证用户输入的密码和数据库的密码是否匹配,如果匹配进行下一步,如果不匹配显示登录失败
5.匹配成功会将当前登录用户的相关信息(包括权限)保存到session会话中,以实现登录状态的保持