springsecurity框架学习
1. springsecurity小试牛刀
1.1 案例-01 实现访问任何资源都要认证
1.1.1 创建一个普通的maven工程
- 工程名:spring-security-demo(很普通的maven工程)
1.1.2 完善工程所需的依赖环境
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 下面的坐标就是 spring-security所需的依赖包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
1.1.3 配置yml文件
server:
port: 8888
1.1.4 准备初始案例代码
- 创建一个controller类,并添加一个普遍的接口方法
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyTestContrl {
@GetMapping("/index")
public String index(){
return "欢迎来到security框架的世界";
}
}
- 添加启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SecurityMainApp {
public static void main(String[] args) {
SpringApplication.run(SecurityMainApp.class, args);
}
}
1.1.5 最终在浏览器中,访问地址,显示的结果
http://localhost:8888/index
为什么在添加了 springsecurity坐标后,什么都不做,再访问controller中的方法时,就会直接跳转到 一个登录页面???
理由:因为导入的pom依赖是:spring-boot-starter-security。这个坐标已经具备了最基础的认证+授权的配置。
1.2 案例-02 实现指定登录帐号和密码效果
- 实现指定登录帐号和密码进行认证的效果,重点是需要对spring-security框架进行配置
springsecurity框架在使用中,有一个非常重要的配置文件,有关它的配置,将全部在这个文件中完成
- 在工程中,创建一个配置文件类,名称定义为 WebSecurityConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration //它是一个配置文件类 --- 类似于以前的xml配置文件
@EnableWebSecurity // 开启springsecurity的web权限控制
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean //提供用户信息,这里没有从数据库查询用户信息,在内存中模拟
public UserDetailsService userDetailsService(){
// 创建一个提供用户信息的接口实现类,这个实现类是基于 InMemory 基于内存
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
//往基于内存的 用户信息提供类中,添加了一条测试数据
inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("123").authorities("admin").build());
return inMemoryUserDetailsManager;
}
@Bean //密码编码器:不加密
public PasswordEncoder passwordEncoder(){//PasswordEncoder 它指的 密码加密的方式
return NoOpPasswordEncoder.getInstance();
}
}
- 实现的效果就是:访问资源时,会先跳转到登录,输入配置文件中的测试帐号后,登录成功就跳转到 访问controller方法
2. springsecurity 登堂入室
- 关于springsecurity进行认证时的核心流程图
传送的核心思想:上面流程图中,每一个Filter都对应着一个具体的功能
- 关于spring-security执行认证的详细流程图
2.1 实现连接自己数据库完成认证
- 实现方式:实现springsecurity的接口UserDetailService
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.Service;
import java.util.ArrayList;
@Service //交给spring创建
public class UserDetailServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//在这里来添加一个测试的用户信息
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
//添加一个测试角色,角色名叫 :admin
//添加权限时,API中提供的方法,是需要传入的是角色名称。注意:这里我们也可以传入用户的权限列表。
//传入权限列表与角色的区别:权限控制的深度
authorities.add(new SimpleGrantedAuthority("admin"));
return new User("admin", "123", authorities);
}
}
2.2 进行数据库连接后的认证
2.2.1 准备数据库sql
- 从提供的资源文件中,导入security.sql脚本。数据库的名称:hrm-security
2.2.2 使用mybatis-plus的代码生成器生成表对应的代码
- 目前需要使用到的表主要是5张
t_role
t_user_role
t_vip_user
t_permission
t_role_permission
2.2.3 导入连接数据库所需的pom依赖
<dependency>
<groupId>itsource.cw</groupId>
<artifactId>hrm-basic-util</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- swagger-ui的依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springsecurity权限框架的环境依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 导入mybatis-plus相关的依赖-->
<!-- 导入数据库连接相关的依赖-->
<!-- 数据库驱动和druid-->
<!-- 注意坑: mybatis-plus3.x后,在自动生成的实体类中,对日期类型进行了升级,使用的是jdk1.8的LocalDate
要支持这种数据类型,必须对连接池druid和mysql驱动进行升级,升级版本如下:
-->
<!-- MySql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
<!-- druid连接池 这个starter的坐标实际上也只是导入了关联版本的druid连接池坐标,所以这里直接用导入druid坐标替代-->
<!--<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.1</version>
</dependency>
<!-- mybatis-plus3的依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
2.2.4 添加yml的配置
#配置数据库的连接
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 连接池指定 springboot2.02版本默认使用HikariCP 此处要替换成Druid
driver-class-name: com.mysql.cj.jdbc.Driver # 这个驱动必须用新版,不能用老版
url: jdbc:mysql:///hrm-security?characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: 123456
druid:
initial-size: 5 # 初始化时建立物理连接的个数
min-idle: 5 # 最小连接池连接数量,最小空闲数量
max-active: 20 # 最大连接池连接数量,最大活跃连接数
max-wait: 60000 # 配置获取连接等待超时的时间
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1
testWhileIdle: true
testOnBorrow: true
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,wall
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
stat-view-servlet:
allow: 0.0.0.0 # 允许哪些IP访问druid监控界面,多个IP以逗号分隔
login-username: admin # 设置登录帐号
login-password: 123456 # 设置登录密码
reset-enable: false # 是否允许重置数据
# url-pattern: /database/* # 默认访问根路径是:/druid/;也可以自定义设置
mybatis-plus: # mybatis的配置。下面的配置希望执行Sql语句可以打印到 控制台
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
2.2.5 改造UserDetailService的接口,根据用户名查询数据库实现认证
import cn.itsource.cw.domain.Permission;
import cn.itsource.cw.domain.VipUser;
import cn.itsource.cw.mapper.VipUserMapper;
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.User;
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.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private VipUserMapper vipUserMapper;
@Override //根据参数中的用户名,来查询用户的对象信息
//1.整个方法体中,并没有对前端传过来的密码进行校验。
//2.前端传过来的用户密码,后台代码根本就没有提供获取的方法。
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
VipUser vipUser = vipUserMapper.findUserByUserName(userName);
//在传入时,会同时查询出当前用户的权限列表
List<GrantedAuthority> authorityList = new ArrayList<>();
//在security进行授权管理时,就要在这里将当前登录用户的权限列表给查出来
List<Permission> permissionList = vipUserMapper.findPermissionListByUserName(userName);
for (Permission permission : permissionList) {
//将当前用户的权限列表查出来,并保存到security
authorityList.add(new SimpleGrantedAuthority(permission.getExpression()));
}
//密码最终校验的地方是由Security框架完成。 原始密码保存在SecurityContext上下文,传入一个加密之后的密码:vipUser.getPassword()
return new User(userName, vipUser.getPassword(), authorityList);
}
}
- 注意:当我们自定义了UserDetailService的实现类后,务必要将配置类WebSecurityConfig.java中的`public UserDetailsService userDetailsService()这个方法注释掉。
2.2.6 编写根据用户名查询权限列表的sql语句
- 编写对应的mapper.java的查询方法
List<Permission> findPermissionListByUserName(String userName);
- 编写对应的sql语句(写在Mapper.xml文件中)
<select id="findPermissionListByUserName" resultType="itsource.cw.domain.Permission" parameterType="string">
select * from t_permission where id in(
select permission_id from t_role_permission where role_id in (
select role_id from t_user_role where user_id in (
select id from t_vip_user where username = #{userName}
)
)
)
</select>
- 测试:同样,访问测试的controller方法,根据数据库中保存的用户信息进行登录
测试时,需要先做一件事:将用户表t_vip_user中的密码,先改成明文,再进行测试
2.2.7 改造WebSercurity的配置,将密码的加密方式改成指定的加密
@Bean //密码编码器
public PasswordEncoder passwordEncoder(){//PasswordEncoder 它指的 密码加密的方式
//return NoOpPasswordEncoder.getInstance();
//按照security默认的加密方式进行加密
return new BCryptPasswordEncoder();
}
- 对BCryptPasswordEncoder加密方式进行测试学习
@Test
public void test1(){
//这个工具类中,提供2个核心方法:加密 + 解密
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//加密
String encode = passwordEncoder.encode("123");
/*
加密之后的结果:
$2a$10$UmYgHJIY7uSmd/3HrLAc4e8FcU/uZpgZPInVoJ6Ca0e.MuZNJtu7G
$2a$10$kbb2wCqCHLJoDTOoXI.tk.wE4dO01z2r2edac8Y79HbgSILmCKm7u
$2a$10$8y7sS7w02e6j3prhR2r4/OpUrGWucF2Q07AKU0A6dU2c8jyzxXbnO
*/
System.out.println(encode);
//解密
boolean matches = passwordEncoder.matches("123", "$2a$10$UmYgHJIY7uSmd/3HrLAc4e8FcU/uZpgZPInVoJ6Ca0e.MuZNJtu7G");
System.out.println(matches);
}
结论:security内置的加密方式,对同一个字符串,每次加密,都会产生不同的加密字符串
- 总结
加密方法:passwordEncoder.encode(原始密码) ----》每次加密,得到的加密字符串可变
校验方法:passwordEncoder.matches(原始密码,加密之后的密码) ----》校验成功,返回true
注意:这种加密,也是不可逆的
3. 授权操作–游刃有余
- 授权的使用是 基于security的全注解完成
3.1 关于3种注解方式
@Secured :它在使用时,需要对添加的角色权限,添加一个前缀 ROLE_
使用特点:@Secured(“ROLE_DEPT”) ,需要拥有部门的角色才能访问,ROLE_前缀是固定的
@PreAuthorize : 它在使用时,无需添加任何前缀,直接通过方法调用的方式使用
使用特点:添加权限时,是通过方法进行操作
@PostAuthorize :这种方式使用较少
重点:掌握 第2种注解的使用
3.2 关于@PreAuthorize的使用流程
3.2.1 开启注解
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启@PreAuthorize注解
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
- 在springsecurity的配置类上,添加注解@EnableGlobalMethodSecurity(prePostEnabled = true),即可让注解生效。
3.2.2 在控制层添加注解实现权限控制
- 在控制层代码中,可以对每一个需要权限控制的方法,添加上权限注解
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
public class EmployeeController {
//zs有这个访问权限,但ls没有;所以zs可访问,但ls无法访问,报403权限不足错误
@GetMapping("/employee/list")
@PreAuthorize("hasAuthority('employee:list')")
public String list() {
return "employee.list";
}
@PostMapping("/employee/add")
//@Secured("ROLE_employee:add")
@PreAuthorize("hasAuthority('employee:add')")
public String add() {
return "employee.add";
}
@PostMapping("/employee/update")
//访问当前访问,只要具备employee:update 或 dept:update 都 可访问
@PreAuthorize("hasAnyAuthority('employee:update','dept:update')")
public String update() {
return "employee.update";
}
@DeleteMapping("/employee/delete")
@PreAuthorize("hasAuthority('employee:delete')")
public String delete() {
return "employee.delete";
}
}
- 重点关注
@PreAuthorize("hasAuthority('employee:add')") 表示访问指定方法,需要有'employee:add'的权限
@PreAuthorize("hasAnyAuthority('employee:update','dept:update')")
表示访问指定方法,需要至少具备 'employee:update','dept:update' 这2个中的任何一个
- 结论
关于授权的方案,在权限控制的实现过程中,我们全部通过 注解完成,不需要写任何java代码
4. 关于对Security认证及授权的代码优化
4.1 自定义登录界面
- 自定义登录界面需要对security配置类,进行相应配置
//授权规则配置
/*
在配置类中,对各种需要权限控制的资源,进行配置的方式 :
只有在程序重启之后,才会生效
注意:在configure方法中,写各项配置,它是有顺序的,写在前端的先生效,写在后面的,如果配置相同,不会生效
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
http.authorizeRequests() //授权配置
// 指定哪些访问的请求地址,不需要认证就可以访问。
.antMatchers("/login", "/login.html").permitAll() //登录路径放行
.anyRequest().authenticated() //其他路径都要认证之后才能访问
.and().formLogin() //允许表单登录
.loginPage("/login.html") //这个就是自定义的登录页面
.loginProcessingUrl("/login") //告诉框架,现在的登录请求的URL地址是:/login
// .usernameParameter("abc")
// .passwordParameter("def")
.successForwardUrl("/loginSuccess") // 设置登陆成功页
.and().logout().permitAll() //登出路径放行 /logout。这是框架自带的登出请求
.and().csrf().disable(); //关闭跨域伪造检查
}
- 注意事项
- 自定义界面调用的方法 .loginPage("/login.html")
- 如果自定义了登录界面,则需要配置当前请求URL .loginProcessingUrl("/login")
- 在登录界面中,如果要将用户的登录帐号和密码正确传给后台的Security接收到 ,参数名不能乱写
默认情况下,框架接收的登录帐号和密码的变量名是:(注意大小写区分)
登录帐号: username
登录密码: password
如果需要自定义,则在配置方法上,进行如下配置:
.usernameParameter("abc")
.passwordParameter("def")
- 创建的登录界面是一个login.html,它必须放在工程的 resources/static目录下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆</title>
</head>
<body>
<h1>这是自定义的登陆界面</h1>
<h2>注意:当自定义登录界面后,传入的帐号和密码的输入框name的名称不能随便写,只能是 username和password</h2>
<form method="post" action="/login">
<div>
用户名:<input type="text" name="username">
</div>
<div>
密码:<input type="password" name="password">
</div>
<div class="checkbox"><!-- rememberme的name的名称使用的也是默认的,不能修改 -->
<label><input type="checkbox" id="rememberme" name="remember-me"/>记住我</label>
</div>
<div>
<button type="submit">立即登陆</button>
</div>
</form>
</body>
</html>
4.2 解决登录失败,给个友好提示
4.2.1 修改配置类,这个配置方法也是写在 configure()方法体中的
.successHandler(new MyAuthenticationSuccessHandler()) //设置认证成功后,自定义处理
.failureHandler(new MyAuthenticationFailureHandler()) //设置认证失败后,自定义处理
4.2.2 登录成功和失败的自定义方法处理类
- MyAuthenticationSuccessHandler.java 成功
import com.alibaba.fastjson.JSON;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
//这个实现类的作用:当security认证成功后,将会执行当前handler处理器
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
//这里经常做的事情:当认证成功后,拼接一个JSON格式的数据返回给请求前端
Map map = new HashMap<>();
map.put("success",true);
map.put("message","认证成功");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-type", "text/html; charset=utf-8");
response.getWriter().print(JSON.toJSONString(map));
response.getWriter().flush();
response.getWriter().close();
}
}
- MyAuthenticationFailureHandler.java 失败
import com.alibaba.fastjson.JSON;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
Map map = new HashMap<>();
map.put("success",false);
map.put("message","认证失败");
//在返回前端数据时,还可以设置一个http的状态(类似:404、403等)
response.setCharacterEncoding("utf-8");
response.setHeader("Content-type", "text/html; charset=utf-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().print(JSON.toJSONString(map));
response.getWriter().flush();
response.getWriter().close();
}
}
4.3 访问某个方法时,提示403异常
4.3.1 在配置类中,添加一个自定义403的异常处理
//配置权限不足时,自定义结果返回给前端
http.exceptionHandling().accessDeniedHandler(new DefaultAccessDeniedHandler());
4.3.2 自定义异常处理类
import cn.itsource.cw.util.AjaxResult;
import com.alibaba.fastjson.JSON;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class DefaultAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
String result = JSON.toJSONString(AjaxResult.me().setSuccess(false).setMessage("无访问权限"));
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.print(result);
writer.flush();
writer.close();
}
}
4.3.3 关于security最终的完整配置类代码如下
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
http.authorizeRequests() //授权配置
.antMatchers("/login", "/login.html").permitAll() //登录路径放行
//进行授权的配置:当要访问资源【/employee/list】,需要当前用户具备权限:employee:list
//现在数据库中,只有zs才有这个权限。最终的效果:张三能正常访问,但李四不可以
//配置访问的资源对应的权限配置,必须要放在.anyRequest().authenticated() 之前
//在配置类中,针对请求的方法进行权限控制:它是全局的。是程序在启动时就已经加载。
//总结:所有资源权限控制,都必须要在这里配置,才能实现全局控制。
// 问题:所有开发人员,要对自己的资源进行控制,都必须改这个配置类,不安全,容易改错。
// .antMatchers("/employee/list/**").hasAuthority("employee:list")
.anyRequest().authenticated() //其他路径都要认证之后才能访问
.and().formLogin() //允许表单登录
.loginPage("/login.html") //这个就是自定义的登录页面
.loginProcessingUrl("/login") //告诉框架,现在的登录请求的URL地址是:/login
// .usernameParameter("abc")
// .passwordParameter("def")
.successForwardUrl("/loginSuccess") // 设置登陆成功页
.successHandler(new MyAuthenticationSuccessHandler())//设置认证成功后,handler的处理器
.failureHandler(new MyAuthenticationFailureHandler())//设置认证失败后handler的处理器
.and().logout().permitAll() //登出路径放行 /logout。这是框架自带的登出请求
.and().csrf().disable(); //关闭跨域伪造检查
// 配置权限不足时,自定义结果返回给前端
http.exceptionHandling().accessDeniedHandler(new DefaultAccessDeniedHandler());
}