6 密码处理
6.1 为什么要加密?
csdn 密码泄露事件
泄露事件经过:https://www.williamlong.info/archives/2933.html
泄露数据分析:https://blog.csdn.net/crazyhacking/article/details/10443849
6.2加密方案
密码加密一般使用散列函数,又称散列算法,哈希函数,这些函数都是单向函数(从明文到密文,反之不行)
常用的散列算法有MD5和SHA
Spring Security提供多种密码加密方案,基本上都实现了PasswordEncoder接口,官方推荐使用BCryptPasswordEncoder
6.3 BCryptPasswordEncoder类初体验
拷贝springsecurity-04-inmemory工程,重命名为springsecurity-05-password-encode
test/java 下新建包com.powernode.password,在该包下新建测试类PasswordEncoderTest,如下
@Slf4j
public class PasswordEncoderTest {
@Test
@DisplayName("测试加密类BCryptPasswordEncoder")
void testPassword(){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//加密(明文到密文)
String encode1 = bCryptPasswordEncoder.encode("123456");
_log_.info("encode1:"+encode1);
String encode2 = bCryptPasswordEncoder.encode("123456");
_log_.info("encode2:"+encode2);
String encode3 = bCryptPasswordEncoder.encode("123456");
_log_.info("encode3:"+encode3);
//匹配方法,判断明文经过加密后是否和密文一样
boolean result1 = bCryptPasswordEncoder.matches("123456", encode1);
boolean result2 = bCryptPasswordEncoder.matches("123456", encode1);
boolean result3 = bCryptPasswordEncoder.matches("123456", encode1);
_log_.info(result1+":"+result2+":"+result3);
_assertTrue_(result1);
_assertTrue_(result2);
_assertTrue_(result3);
}
}
查看控制台发现特点是:**相同的字符串加密之后的结果都不一样,但是比较的时候是一样的,因为加了盐(**salt)了。
上面简单看下即可
小提示:
Ø 开发代码时不允许使用main方法测试,而是使用单元测试来测试
Ø 代码中一般不允许使用System.out.println 直接输出,而是使用日志输出
Ø 单元测试尽量使用断言,而不是使用System.out.println输出
6.4 使用加密器并且加密
修改MySecurityUserConfig类中的加密器bean
@Bean
public PasswordEncoder passwordEncoder(){
//使用加密算法对密码进行加密
return new BCryptPasswordEncoder();
}
启动程序测试,发现不能正常登录
原因是输入的密码是进行加密了,但是系统中定义的用户密码没有加密
将系统定义的用户密码修改成密文,如下
@Configuration
public class MySecurityUserConfig {
@Bean
public UserDetailsService userDetailService() {
// 使用org.springframework.security.core.userdetails.User类来定义用户
//定义两个用户
UserDetails user1 = User._builder_()
.username("eric")
.password(passwordEncoder().encode("123456"))
.roles("student")
.build();
UserDetails user2 = User._builder_()
.username("thomas")
.password(passwordEncoder().encode("123456"))
.roles("teacher")
.build();
//创建两个用户
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(user1);
userDetailsManager.createUser(user2);
return userDetailsManager;
}
/*
* 从 Spring5 开始,强制要求密码要加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
//使用加密算法对密码进行加密
return new BCryptPasswordEncoder();
}
}
重启程序,再次测试即可,发现登录和访问没问题了
7 查看当前登录用户信息及配置用户权限
复制springsecurity-05-password-encode,复制后为springsecurity-06-loginuser-info
7.1 获取当前登录用户信息
新建一个controller
@RestController
public class CurrentLoginUserInfoController {
_/**
* 从当前请求对象中获取
*/
_@GetMapping("/getLoginUserInfo")
public Principal getLoginUserInfo(Principal principle){
return principle;
}
_/**
*从当前请求对象中获取
*/
_@GetMapping("/getLoginUserInfo1")
public Authentication getLoginUserInfo1(Authentication authentication){
return authentication;
}
_/**
* 从安全应用上下文(SecurityContextHolder)获取安全应用上下文(SecurityContext),从安全应用上下文中获取认证信息
* **@return
***/
_@GetMapping("/getLoginUserInfo2")
public Authentication getLoginUserInfo(){
Authentication authentication = SecurityContextHolder._getContext_().getAuthentication();
return authentication;
}
}
注意Authentication接口继承自 Principal
重启程序,访问
http://localhost:8080/getLoginUserInfo
http://localhost:8080/getLoginUserInfo1
http://localhost:8080/getLoginUserInfo2
运行结果
{
“authorities”: [{
“authority”: “ROLE_teacher”
}],
“details”: {
“remoteAddress”: “0:0:0:0:0:0:0:1”,
“sessionId”: “34E452050095348E6306CF95B2025CD9”
},
“authenticated”: true,
“principal”: {
“password”: null,
“username”: “thomas”,
“authorities”: [{
“authority”: “ROLE_teacher”
}],
“accountNonExpired”: true,
“accountNonLocked”: true,
“credentialsNonExpired”: true,
“enabled”: true
},
“credentials”: null,
“name”: “thomas” }
Principal 定义认证的而用户,如果用户使用用户名和密码方式登录,principal通常就是一个UserDetails(后面再说)
Credentials:登录凭证,一般就是指密码。当用户登录成功之后,登录凭证会被自动擦除,以防泄露。
authorities:用户被授予的权限信息。
7.2 配置用户权限
配置用户权限有两种方式:
配置roles
配置authorities
注意事项:
如果给一个用户同时配置roles和authorities,哪个写在后面哪个起作用
配置roles时,权限名会加上ROLE_。
修改WebSecurityConfig代码中的
// 注意 1 哪个写在后面哪个起作用 2 角色变成权限后会加一个ROLE_前缀,比如ROLE_teacher
// UserDetails user2 = User.builder()
// .username("thomas")
// .password(passwordEncoder().encode("123456"))
// .authorities("teacher:add","teacher:update")
// .roles("teacher")
// .build();
UserDetails user2 = User._builder_()
.username("thomas") .password(passwordEncoder().encode("123456"))
.roles("teacher")
.authorities("teacher:add","teacher:update")
.build();
重启程序使用thomas登录,然后查看用户认证信息
http://localhost:8080/getLoginUserInfo |
---|
可以看到authorities的情况。
从设计层面讲,角色和权限是两个完全不同的东西
从代码层面来讲,角色和权限并没有太大区别,特别是在Spring Security中
8 授权(对URL进行授权)
上面讲的实现了认证功能,但是受保护的资源是默认的,默认所有认证(登录)用户均可以访问所有资源,不能根据实际情况进行角色管理,要实现授权功能,需重写WebSecurityConfigureAdapter 中的一个configure方法
复制springsecurity-06-loginuser-info 工程,然后改名为springsecurity-07-url
新建WebSecurityConfig类,重写configure(HttpSecurity http)方法
WebSecurityConfig 完整代码如下:
@Configuration
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//角色student或者teacher都可以访问/student/** 这样的url
.mvcMatchers("/student/*").hasAnyRole("student", "teacher")
// 角色teacher 可以访问teacher/**
.mvcMatchers("/teacher/**").hasRole("teacher")
//权限admin:query 可以访问/admin**
// .mvcMatchers("/admin/**").hasAuthority("admin:query")
//角色teacher 或者权限admin:query 觉可以访问admin/**
.mvcMatchers("/admin/**").access("hasRole('teacher') or hasAuthority('admin:query')")
//任何请求均需要认证
.anyRequest().authenticated();
//使用表单登录
http.formLogin();
}
}
使用admin登录,访问
http://localhost:8080/teacher/query
http://localhost:8080/student/query
http://localhost:8080/admin/query
分别查看效果,实现权限控制
上面是对URL资源进行控制,就是哪些权限可以访问哪些URL。
9 授权(方法级别的权限控制)
上面学习的认证与授权都是基于URL的,我们也可以通过注解灵活的配置方法安全,我们先通过@EnableGlobalMethodSecurity开启基于注解的安全配置。
9.1 新建模块springsecurity-08-method
9.2 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
9.3 新建启动类并复制 CurrentLoginUserInfoController类
新建启动类Application,学员自行创建
9.4 新建service及其实现
com.powernode.service 新建教师接口
public interface TeacherService {
String add();
String update();
String delete();
String query();
}
com.powernode.service.impl 实现接口
@Service
@Slf4j
public class TeacherServiceImpl implements TeacherService {
@Override
public String add() {
_log_.info("添加教师成功");
return "添加教师成功";
}
@Override
public String update() {
_log_.info("修改教师成功");
return "修改教师成功";
}
@Override
public String delete() {
_log_.info("删除教师成功");
return "删除教师成功";
}
@Override
public String query() {
_log_.info("查询教师成功");
return "查询教师成功";
}
}
9.5 修建TeacherController
@RestController
@RequestMapping("/teacher")
public class TeacherController {
@Resource
private TeacherService teacherService;
@GetMapping("/query")
public String queryInfo() {
return teacherService.query();
}
@GetMapping("/add")
public String addInfo() {
return teacherService.add();
}
@GetMapping("/update")
public String updateInfo() {
return teacherService.update();
}
@GetMapping("/delete")
public String deleteInfo() {
return teacherService.delete();
}
}
9.6 新建安全配置类
com.powernode.config下新建用户配置类
@Configuration
public class MySecurityUserConfig {
@Bean
public UserDetailsService userDetailService() {
// 使用org.springframework.security.core.userdetails.User类来定义用户
//定义两个用户
UserDetails user1 = User._builder_()
.username("eric")
.password(passwordEncoder().encode("123456"))
.roles("student")
.build();
UserDetails user2 = User._builder_()
.username("thomas")
.password(passwordEncoder().encode("123456"))
.roles("teacher")
.build();
UserDetails user3 = User._builder_()
.username("admin")
.password(passwordEncoder().encode("123456"))
.authorities("teacher:add", "teacher:update")
.roles("teacher")
.build();
//创建两个用户
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(user1);
userDetailsManager.createUser(user2);
return userDetailsManager;
}
/*
* 从 Spring5 开始,强制要求密码要加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
//使用加密算法对密码进行加密
return new BCryptPasswordEncoder();
}
}
新建WebSecurityConfig类
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {//任何访问均需要认证
http.authorizeRequests().anyRequest().authenticated();
http.formLogin(); //使用表单登陆方式
}
}
9.7 启动程序并访问
访问以下地址
| http://localhost:8080/teacher/add
http://localhost:8080/teacher/update
http://localhost:8080/teacher/delete
http://localhost:8080/teacher/query |
---|
通过admin或thomas登录均可以访问所有资源
9.8 修改安全配置类WebSecurityConfig
加上启用全局方法安全注解
@EnableGlobalMethodSecurity(prePostEnabled = true) |
---|
修改后,完整代码如下:
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//任何访问均需要认证
http.authorizeRequests().anyRequest().authenticated();
//使用表单登录
http.formLogin();
}
}
9.9 修改TeacherServiceImpl
在每个方法上加上前置授权注解:@PreAuthorize
完整代码如下:
@Service
@Slf4j
public class TeacherServiceImpl implements TeacherService {
@Override
@PreAuthorize("hasAuthority('teacher:add') OR hasRole('teacher')")
public String add() {
_log_.info("添加教师成功");
return "添加教师成功";
}
@Override
@PreAuthorize("hasAuthority('teacher:update')")
public String update() {
_log_.info("修改教师成功");
return "修改教师成功";
}
@Override
@PreAuthorize("hasAuthority('teacher:delete')")
public String delete() {
_log_.info("删除教师成功");
return "删除教师成功";
}
@Override
@PreAuthorize("hasRole('teacher')")
public String query() {
_log_.info("查询教师成功");
return "查询教师成功";
}
}
9.10 启动并运行
运行程序分别使用admin和teacher登录,可以查看不同效果
http://localhost:8080/teacher/add
http://localhost:8080/teacher/update
http://localhost:8080/teacher/delete
http://localhost:8080/teacher/query
发现thomas可以访问添加和查询,别的不能访问,amdin可以访问添加和更新,别的不能访问。
代码说明:
Ø EnableGlobalMethodSecurity注解的属性prePostEnabled = true 解锁@PreAuthorize 和@PostAuthorize注解,@PreAuthorize 在方法执行前进行验证,@PostAuthorize 在方法执行后进行验证
Ø EnableGlobalMethodSecurity的securedEnabled = true 解锁@Secured注解,@Secured和@PreAuthorize用法基本一样 @Secured对应的角色必须要有ROLE_前缀
10 SpringSecurity 返回json
前后端分离成为企业应用开发中的主流,前后端分离通过json进行交互,登录成功和失败后不用页面跳转,而是一段json提示
10.1 新建模块springsecurity-09-json
10.2 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
10.3 新建三个controller和获取登录用户信息的controller
参照1.2.4 创建,可以直接拷贝过来
10.4 新建启动类
com.powernode下新建Application类,学员自行创建
10.5 创建统一响应类HttpResult
在com.powernode.vo中创建该类
@Data
@AllArgsConstructor
@NoArgsConstructor@Builder
public class HttpResult {
private Integer code;
private String msg;
private Object data;
public HttpResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
10.6 创建登录成功处理器
com.powernode.config 包下创建
@Component
public class MyAutheticationSuccessHandle implements AuthenticationSuccessHandler {@Resource
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
HttpResult httpResult = new HttpResult(200, "登录成功", authentication);
String str = objectMapper.writeValueAsString(httpResult);
response.getWriter().write(str);
response.getWriter().flush();
}
}
10.7 创建登录失败处理器
_/**
* 登陆失败的处理器
*/
_@Component@Slf4jpublic class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Resource
private ObjectMapper objectMapper;
_/**
* **@param **request 当前的请求对象
* **@param **response 当前的响应对象
* **@param **exception 失败的原因的异常
* **@throws **IOException
* **@throws **ServletException
*/
_@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System._err_.println("登陆失败");
//设置响应编码
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
//返回JSON出去
HttpResult result=HttpResult.builder() .code(-1) .msg("登录失败") .build(); if(exception instanceof BadCredentialsException){
result.setData("密码不正确");
}else if(exception instanceof DisabledException){
result.setData("账号被禁用");
}else if(exception instanceof UsernameNotFoundException){
result.setData("用户名不存在");
}else if(exception instanceof CredentialsExpiredException){
result.setData("密码已过期");
}else if(exception instanceof AccountExpiredException){
result.setData("账号已过期");
}else if(exception instanceof LockedException){
result.setData("账号被锁定");
}else{
result.setData("未知异常");
}
//把result转成JSON
String json = objectMapper.writeValueAsString(result);
//响应出去
PrintWriter out = response.getWriter();
out.write(json);
out.flush();
}
}
10.8 创键无权限处理器
_/**
* 无权限的处理器
*/
_@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
//声明一个把对象转成JSON的对象@Resource
private ObjectMapper objectMapper;
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//设置响应编码
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
//创建响应对象
HttpResult result= HttpResult.builder()
.code(-1)
.msg("用户没有访问权限")
.build(); //把result转成JSON
String json = objectMapper.writeValueAsString(result);
//响应json出去
PrintWriter out = response.getWriter();
out.write(json);
out.flush();
}
}
10.9 创建登出(退出)处理器
_/**
* 退出成功的处理器
*/
_@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
//声明一个把对象转成JSON的对象@Resource
private ObjectMapper objectMapper;
_/**
*
* **@param **request
* **@param **response
* **@param **authentication 当前退出的用户对象
* **@throws **IOException
* **@throws **ServletException
*/
_@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System._out_.println("退出成功");
//设置响应编码
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
//返回JSON出去
HttpResult result= HttpResult.builder() .code(200) .msg("退出成功") .build();
//把result转成JSON
String json = objectMapper.writeValueAsString(result);
//响应出去
PrintWriter out = response.getWriter();
out.write(json);
out.flush();
}
}
10.10 创建用户配置类
@Configuration
public class MySecurityUserConfig {
@Bean
public UserDetailsService userDetailService() {
// 使用org.springframework.security.core.userdetails.User类来定义用户
//定义用户
UserDetails user1 = User._builder_()
.username("eric")
.password(passwordEncoder().encode("123456"))
.roles("student")
.build();
UserDetails user2 = User._builder_()
.username("thomas")
.password(passwordEncoder().encode("123456"))
.roles("teacher")
.build();
UserDetails user3 = User._builder_()
.username("admin")
.password(passwordEncoder().encode("123456"))
.roles("admin")
.build();
//创建用户
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(user1);
userDetailsManager.createUser(user2);
userDetailsManager.createUser(user3);
return userDetailsManager;
}
/*
* 从 Spring5 开始,强制要求密码要加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
//使用加密算法对密码进行加密
return new BCryptPasswordEncoder();
}
}
10.11 安全配置类WebSecurityConfig
```java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 注入登陆成功的处理器
@Autowired
private MyAutheticationSuccessHandle successHandler;
// 注入登陆失败的处理器
@Autowired
private MyAuthenticationFailureHandler failureHandler;
// 注入没有权限的处理器
@Autowired
private MyAccessDeniedHandler accessDeniedHandler;
// 注入退出成功的处理器
@Autowired
private MyLogoutSuccessHandler logoutSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置拒绝访问处理器 http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
//配置登录成功处理器,配置登录失败处理器 http.formLogin().successHandler(successHandler).failureHandler(failureHandler); //配置退出成功处理器
http.logout().logoutSuccessHandler(logoutSuccessHandler);
http.authorizeRequests().mvcMatchers("/teacher/**").hasRole("teacher").anyRequest().authenticated();
}
}
10.12 启动程序
10.13 访问测试
可以使用admin用户实验登录失败、登录成功、退出和访问http://localhost:8080/teacher/query 查看无权访问的效果
11 使用自定义UserDetailsService实现获取用户认证信息
11.1 新建子模块springsecurity-10-userdetailservice
11.2 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
11.3 新建启动类
com.powernode包下新建启动类Application,学员自行创建
11.4 新建三个controller
参照1.2.4 创建,可以直接拷贝过来
11.5 新建获取登录用户认证信息的controller
拷贝7.1 即可
11.6 新建用户信息类
com.powernode.vo包下新建SecurityUser 类,该类实现接口UserDetails接口
public class SecurityUser implements UserDetails {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
//用户密码使用密文 return new BCryptPasswordEncoder().encode("123456");
}
@Override
public String getUsername() {//定义用户名
return "thomas";
}
@Override
public boolean isAccountNonExpired() {//账号是否未过期,返回true 未过期
return true;
}
@Override
public boolean isAccountNonLocked() {//账号是否未锁定,返回true 未锁定
return true;
}
@Override
public boolean isCredentialsNonExpired() {//凭据(凭证),目前可以理解成密码,是否未过期,返回true 未过期
return true;
}
@Override
public boolean isEnabled() {//账号是否可以,返回true可用
return true;
}
}
代码说明:
用户实体类需要实现UserDetails接口,并实现该接口中的7个方法, UserDetails 接口的7个方法如下图:
11.7 新建类实现UserDetailService接口
com.powernode.service.impl 包下新建UserServiceImpl 实现UserDetailService
@Service
public class UserServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SecurityUser securityUser= new SecurityUser();
if(username==null || !username.equals(securityUser.getUsername())){
throw new UsernameNotFoundException("该用户不存在");
}
return securityUser;
}
}
11.8 新建安全配置类
com.powernode.config下新建WebSecurityConfig类,配置密码编码器
@Configuration
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
启动程序,并使用浏览器访问程序:http://localhost:8080/student/query
发现需要登录,使用thomas/123456 登录后,即可正常访问。
访问:http://localhost:8080/getLoginUserInfo
发现该用户并没有权限信息
11.9 配置用户权限信息
修改SecurityUser类中的getAuthorities 方法
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
GrantedAuthority g1=()->"student:query"; //使用lambda表达式创建接口实现类,而不是使用匿名内部类来实现接口
// GrantedAuthority g1=new SimpleGrantedAuthority("student:query"); // 使用子类创建对象
List<GrantedAuthority> grantedAuthorityList=new ArrayList<>();
grantedAuthorityList.add(g1);
return grantedAuthorityList;
}
11.10 修改要访问controller中的方法需要哪些权限
修改WebSecurityConfig,添加全局方法拦截注解@EnableGlobalMethodSecurity(prePostEnabled = true)
注意可以去掉:@Configuration注解了
修改StudentController
添加 @PreAuthorize(“hasAuthority(‘student:query’)”) 注解修改后如下:
@RestController
@RequestMapping("/student")
public class StudentController {
@GetMapping("/query")
@PreAuthorize("hasAuthority('student:query')")
public String queryInfo(HttpServletRequest request){
return "I am a student,My name is Eric";
}
}
修改TeacherController
添加 @PreAuthorize(“hasAuthority(teacher:query’)”) 注解修改后如下:
@RestController
@RequestMapping("/teacher")
public class TeacherController {
@GetMapping("/query")
@PreAuthorize("hasAuthority('teacher:query')")
public String queryInfo(){
return "I am a teacher,My name is Thomas";
}
}
启动测试,使用thomas/123456 登录系统,发现可以访问student/query,不可以访问teacher/query
再次访问:http://localhost:8080/getLoginUserInfo 查看用户信息
11.11 为什么讲这个示例?
是为了使用数据库存储用户角色权限信息做准备,只要从数据库中取出数据存储到实现UserDetails 的接口的类中即可,比如SecurityUser 中即可。
12 基于数据库的认证
12.1 创建数据库security_study和表
创建数据库security_study
导入数据库脚本security_study.sql
12.2 创建模块springsecurity-11-database-authentication
12.3 添加依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.13</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<java.version>8</java.version>
</properties>
<dependencies>
<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>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
12.4 配置数据源和mybatis
新建配置文件application.yml并配置数据源和mybatis
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
mybatis:
type-aliases-package: com.powernode.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/*.xml
12.5 新建各个包
12.6 新建用户实体类
com.powernode.entity 包下新建用户实体类
@Data@AllArgsConstructor@NoArgsConstructor@Builder
public class SysUser implements Serializable {
private Integer userId;
private String username;
private String password;
private String sex;
private String address;
private Integer enabled;
private Integer accountNoExpired;
private Integer credentialsNoExpired;
private Integer accountNoLocked;
}
12.7 新建用户mapper和映射文件
com.powernode.dao下新建
public interface SysUserDao {
_/**
* 根据用户名获取用户信息
* **@param **username
* **@return
***/
_SysUser getByUserName(@Param("username") String username);
}
mapper下新建映射文件SysUserMapper.xml
<?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.powernode.dao.SysUserDao">
<select id="getByUserName" resultType="sysUser">
select user_id,username,password,sex,address,enabled,account_no_expired,credentials_no_expired,account_no_locked
from sys_user where username=#{username}
</select>
</mapper>
注意select后面不要使用*。
12.8 新建启动类
com.powernode包下新建启动类
@SpringBootApplication
@MapperScan("com.powernode.dao")
public class Application {
public static void main(String[] args) {
SpringApplication._run_(Application.class,args);
}
}
12.9 单元测试
测试dao
@SpringBootTest
class SysUserDaoTest {
@Resource
private SysUserDao sysUserDao;
@Test
void getByUserName() {
SysUser sysUser = sysUserDao.getByUserName("obama");
_assertNotNull_(sysUser);
}
}
注意单元测试要测试哪些:dao–service-controller,实体类一般不需要测试
12.10 新建安全用户类
com.powernode.vo包下新建类
public class SecurityUser implements UserDetails {
private final SysUser sysUser;
public SecurityUser(SysUser sysUser) {
this.sysUser=sysUser;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
String userPassword=this.sysUser.getPassword();
//注意清除密码
this.sysUser.setPassword(null);
return userPassword;
}
@Override
public String getUsername() {
return sysUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return sysUser.getAccountNoExpired().equals(1);
}
@Override
public boolean isAccountNonLocked() {
return sysUser.getAccountNoLocked().equals(1);
}
@Override
public boolean isCredentialsNonExpired() {
return sysUser.getCredentialsNoExpired().equals(1);
}
@Override
public boolean isEnabled() {
return sysUser.getEnabled().equals(1);
}
}
12.11新建UserServiceImpl 实现UserDetailService接口
@Service
public class UserServiceImpl implements UserDetailsService {
@Resource
private SysUserDao sysUserDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserDao.getByUserName(username);
if(null==sysUser){
throw new UsernameNotFoundException("账号不存在");
}
return new SecurityUser(sysUser);
}
}
12.12 新建service单元测试
@SpringBootTest
class UserServiceImplTest {
@Resource
private UserServiceImpl userService;
@Test
void loadUserByUsername() {
UserDetails userDetails = userService.loadUserByUsername("obama");
_assertNotNull_(userDetails);
}
}
12.13 新建两个控制器
@RestController
@Slf4j
@RequestMapping("/student")
public class StudentController {
@GetMapping("/query")
public String queryInfo(){
return "query student";
}
@GetMapping("/add")
public String addInfo(){
return "add student!";
}
@GetMapping("/update")
public String updateInfo(){
return "update student";
}
@GetMapping("/delete")
public String deleteInfo(){
return "delete student!";
}
@GetMapping("/export")
public String exportInfo(){
return "export student!";
}
}
@RestController
@Slf4j
@RequestMapping("/teacher")
public class TeacherController {
@GetMapping("/query")
@PreAuthorize("hasAuthority('teacher:query')")
public String queryInfo(){
return "I am a teacher!";
}
}
12.14 新建获取登录用户认证信息的controller
从7.1 中拷贝即可
12.15 新建web安全配置类
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
http.formLogin();
}
}
启动并进行各种测试
使用thomas和obama分别登录测试,发现student/query等能访问(不需要访问权限),teacher/query 不能访问(需要访问权限),原因
http://localhost:8080/getLoginUserInfo |
---|
发现用户没有权限,但是/teacher/query 需要访问权限,所以/teacher/query 无法访问
13 基于数据库的方法授权
13.1 新建模块
复制springsecurity-11-database-authentication 改名为springsecurity-12-database-authorization-method
注意这个工程已经有认证功能了。下面咱们看下如何设置用户的权限
13.2 新建菜单(权限)实体类
@Data@AllArgsConstructor@NoArgsConstructor@Builder
public class SysMenu implements Serializable {
private Integer id;
private Integer pid;
private Integer type;
private String name;
private String code;
}
13.3 新建权限mapper和映射文件
public interface SysMenuDao {
List<String> queryPermissionByUserId(@Param("userId") Integer userId);
}
映射文件SysMenuMapper.xml
<?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.powernode.dao.SysMenuDao">
<select id="queryPermissionByUserId" resultType="string">
SELECT distinct sm.`code` FROM `sys_role_user` sru inner join sys_role_menu srm
on sru.rid=srm.rid inner join sys_menu sm on srm.mid=sm.id where sru.uid=#{userId} and sm.delete_flag=0
</select>
</mapper>
13.4 权限dao的单元测试
@SpringBootTest
class SysMenuDaoTest {
@Resource
private SysMenuDao sysMenuDao;
@Test
void queryPermissionByUserId() {
List<String> menuList = sysMenuDao.queryPermissionByUserId(1);
_assertTrue_(!menuList.isEmpty());
}
}
13.5 修改SecurityUser实体类
加入一个属性
private List simpleGrantedAuthorities; |
---|
修改方法getAuthorities
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return simpleGrantedAuthorities;
}
添加一个set方法
public void setSimpleGrantedAuthorities(List<SimpleGrantedAuthority> simpleGrantedAuthorities) {
this.simpleGrantedAuthorities = simpleGrantedAuthorities;
}
13.6 修改UserServiceImpl
增加设置权限的步骤,修改后如下:
@Service
@Slf4j
public class UserServiceImpl implements UserDetailsService {
@Resource
private SysUserDao sysUserDao;
@Resource
private SysMenuDao sysMenuDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserDao.getByUserName(username);
if(null==sysUser){
throw new UsernameNotFoundException("账号不存在");
}
List<String> strList=sysMenuDao.queryPermissionByUserId(sysUser.getUserId());//使用stream流来转换// SimpleGrantedAuthority::new 相当于调用构造方法
List<SimpleGrantedAuthority> grantedAuthorities=strList.stream().map(SimpleGrantedAuthority::new).collect(_toList_());
SecurityUser securityUser = new SecurityUser(sysUser);
securityUser.setSimpleGrantedAuthorities(grantedAuthorities);
return securityUser;
}
}
启动并进行各种测试
使用thomas和obama分别登录测试,发现已经有权限功能了