Spring Security详解(2.6.7版本)
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
简介
- Spring Security是基于Spring生态圈的,用于提供安全访问控制解决方案的框架。
- Spring Security的安全管理有两个重要概念,分别是Authentication(认证)和Authorization(授权)。
Spring Security核心功能
- 用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能够访问该系统。通俗点说就是系统认为用户是否能登录。
- 用户授权指的是:验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。通俗点讲就是系统判断用户是否有权限去做某些事情。
入门
准备工作
数据库
我们用到两张表
t_role有三个数据
t_account是一张空表,后面会填入
导入maven依赖
<dependencies> <!--热部署--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <!--mybatis-plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency> <!--jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--security--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--thymeleaf模板--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!--security整合thymeleaf--> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency> <!--web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--test--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
application.yaml
spring:
thymeleaf:
cache: false
datasource:
url: jdbc:mysql://localhost:3306/springbootdata?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 整合mybatis
mybatis:
type-aliases-package: com.zhao.pojo
mapper-locations:
- classpath:mybatis/mapper/*.xml
创建一个控制器测试效果
@Controller public class IndexController { @GetMapping("/hello") @ResponseBody public String hello(){ return "Hello,visitor"; } }
启动
启动后后台会给出一个随机密码
账号是user,密码就是上面这一串
登录之后可以看见在控制器中输出的内容
这种默认安全管理方式存在诸多问题,例如:
只有唯一的默认登录用户user、密码随机生成且过于暴露、登录页面及错误提示页面不是我们想要的等。
自定义用户认证
1、内存身份认证(基本就是测试的时候使用)
//AOP : 拦截器! @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();//密码加密 //内存身份认证 auth.inMemoryAuthentication().passwordEncoder(encoder) .withUser("zhao") //用户的账号由默认的user改为自己设置的zhao .password(encoder.encode("123456")) //由随机密码改为自己设置的123456,encoder.encode()是加密操作,不加密的话会报错 .roles("common") //赋予角色(先不管) .and() //下面的为第二个用户 .withUser("lily") .password(encoder.encode("123456")) .roles("vip"); } }
2、JDBC身份认证(也用的不多)
我用Mybatis-Plus把用户信息写入了数据库,如果不会使用MP,可以用着mybatis就可以
MP使用还是挺简单的,可以去看Mybatis-Plus详解,只用看这一篇就够了
@Test void contextLoads() { Account account1 = new Account(); account1.setAccount("张三").setPassword(new BCryptPasswordEncoder().encode("123456")).setRoleId(1); Account account2 = new Account(); account2.setAccount("李四").setPassword(new BCryptPasswordEncoder().encode("123456")).setRoleId(2); Account account3 = new Account(); account3.setAccount("王五").setPassword(new BCryptPasswordEncoder().encode("123456")).setRoleId(3); accountMapper.insert(account1); accountMapper.insert(account2); accountMapper.insert(account3); }
查看数据库里面的t_account表
jbdc认证,userSQL和authoritySQL查出来的要一一对应
//AOP : 拦截器! @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired DataSource dataSource; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();//密码加密 //JDBC身份认证 // 根据用户名获取用户信息,account是账号,password是密码,valid是验证用户是否有效的 String userSQL="select account,password,valid from t_account where account=?"; //根据用户名获取用户权限的SQL String authoritySQL="select a.account,r.role_code " + "from t_account a,t_role r " + "where a.role_id=r.id and a.account=? "; auth.jdbcAuthentication().passwordEncoder(encoder) .dataSource(dataSource) .usersByUsernameQuery(userSQL) .authoritiesByUsernameQuery(authoritySQL); } }
自己测试一下就行了
3、自定义用户认证(重要,用的最多的)
定义UserDetailsService用于封装认证用户信息
@Service public class CustomUserDetailsService implements UserDetailsService { @Autowired AccountMapper accountMapper; @Autowired RoleMapper roleMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //使用mybatis-plus从数据库中读取用户信息 LambdaQueryWrapper<Account> wrapper = new LambdaQueryWrapper<>(); //这里我没在MP中讲到,LambdaQueryWrapper只是是需要使用Lambda语法使用Wrapper,如Account::getAccount,就不用写成死的字符串:wrapper.eq("account",username),提高了代码的可维护性,高聚合 wrapper.eq(Account::getAccount,username); Account account = accountMapper.selectOne(wrapper); LambdaQueryWrapper<Role> wrapper2 = new LambdaQueryWrapper<>(); wrapper2.eq(Role::getId,account.getRoleId()); Role role = roleMapper.selectOne(wrapper2); //将用户信息封装成User返回 if (account==null){ throw new UsernameNotFoundException("用户不存在"); }else { ArrayList<GrantedAuthority> list = new ArrayList<>(); // list.add(new SimpleGrantedAuthority("ROLE_"+role.getRole_code())); list.add(new SimpleGrantedAuthority(role.getRoleCode())); User user = new User(account.getAccount(), account.getPassword(), list); return user; } } }
加入自定义类中
//AOP : 拦截器! @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired CustomUserDetailsService customUserDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();//密码加密 //自定义身份认证 auth.userDetailsService(customUserDetailsService).passwordEncoder(encoder); } }
自己测试一下就行了
自定义授权管理
在系统中,通常需要对角色进行权限控制,使得不同用户具有不同的操作权限,在Security中,通过重写WebSecurityConfigurerAdapter类的configure(HttpSecurity http)方法,可以实现用户访问控制
//链式编程 @Override //授权 public void configure(HttpSecurity http) throws Exception { http.authorizeHttpRequests() .antMatchers("/").permitAll() //所有人可以访问 / 的路径 .antMatchers("/admin/**").hasAuthority("admin") //只有拥有admin的授权就可以访问 /admin/** 后面的路径 .antMatchers("/purchaser/**").hasAnyAuthority("admin","purchaser") //只有拥有admin和purchaser的授权就可以访问 /purchaser/** 后面的路径 .antMatchers("/shopkeeper/**").hasAuthority("shopkeeper"); //跳转到自己设置的首页,不用springsecurity自带的登录页面 http.formLogin().loginPage("/toLogin").loginProcessingUrl("/login"); //防止网站攻击:http.csrf() get post http.csrf().disable();//关闭csrf功能,可以注销(关闭防止网站攻击功能)可以接收get请求,否则不能接收get请求,这里先不管,后面我会具体说 csrf 的操作 //注销,开了注销功能,跳到首页,默认的就是/logout,可以不加,但也可以修改为自己喜欢的链接 http.logout().logoutUrl("/logout").logoutSuccessUrl("/"); //开启记住我功能 cookie,默认保存两周,自定义接收前端的参数 http.rememberMe().rememberMeParameter("remember"); }
HttpSecurity类提供了Http请求的限制以及权限、Session管理配置、CSRF跨站请求问题等方法,如下表所示:
在主页面实现退出功能(/logout)
<a th:href="@{/logout}" 退出登录</a>
在主页面实现记住我功能(name=“remember”),后面有个板块详细讲解记住我功能,这里混个眼熟
<input type="checkbox" name="remember"> 记住我
我自己写的Controller,对应下面的Security配置,方便大家看
@Controller public class IndexController { //下面这一堆都是为了测试 用户授权管理 @RequestMapping("/goods") @ResponseBody public String goods(){ return "查看商品列表"; } @RequestMapping("/purchaser/buy") @ResponseBody public String buy(){ return "加入购物车并支付"; } @RequestMapping("/admin/goods/check") @ResponseBody public String goodsCheck(){ return "审核发布商品"; } @RequestMapping("/admin/shop/check") @ResponseBody public String check(){ return "审核店铺信息"; } @RequestMapping("/shopkeeper/publish") @ResponseBody public String publish(){ return "店主发布商品信息"; } @RequestMapping({"/","/index"}) //直接访问 / 或者 /index ,就可以进入 index 页面,这里我是为了方便,比如直接输入 localhost:8080 就可以直接进入 index 页面 public String index(){ return "index"; } @RequestMapping({"/toLogin"}) //为了代替Security自带的 login 登录页面 public String toLogin(){ return "views/login"; } }
Security配置
//AOP : 拦截器! @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired CustomUserDetailsService customUserDetailsService; //链式编程 @Override //授权 public void configure(HttpSecurity http) throws Exception { http.authorizeHttpRequests() .antMatchers("/").permitAll() //所有人可以访问 / 的路径 .antMatchers("/admin/**").hasAuthority("admin") //只有拥有admin的授权就可以访问 /admin/** 后面的路径 .antMatchers("/purchaser/**").hasAnyAuthority("admin","purchaser") //只有拥有admin和purchaser的授权就可以访问 /purchaser/** 后面的路径 .antMatchers("/shopkeeper/**").hasAuthority("shopkeeper"); //跳转到自己设置的首页,不用springsecurity自带的登录页面 http.formLogin().loginPage("/toLogin").loginProcessingUrl("/login"); //防止网站攻击:http.csrf() get post http.csrf().disable();//关闭csrf功能,可以注销(关闭防止网站攻击功能)可以接收get请求 //注销,开了注销功能,跳到首页 http.logout().logoutSuccessUrl("/"); //开启记住我功能 cookie,默认保存两周,自定义接收前端的参数 http.rememberMe().rememberMeParameter("remember"); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();//密码加密 //自定义身份认证 auth.userDetailsService(customUserDetailsService).passwordEncoder(encoder); } }
测试:403就代表没有权限,就说明操作成功了
登录用户信息获取
在Spring Security中,三种方式可以获取登录用户信息:
通过HttpSession获取
通过SecurityContextHolder获取
通过Authentication获取
通过HttpSession获取
SecurityContext接口:安全上下文。即存储认证授权的相关信息,实际上就是存储”当前用户”账号信息和相关权限。这个接口只有两个方法,Authentication对象的getter、setter。
Authentication对象:通过认证的对象,通过getPrincipal()方法可以获取UserDetails接口实例。
@RequestMapping("/main") public String main(HttpSession session){ SecurityContext context = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT"); UserDetails details = (UserDetails) context.getAuthentication().getPrincipal(); System.out.println(details.getUsername()); System.out.println(details.getPassword()); System.out.println(details.getAuthorities()); return "这里我获取了用户名,用户密码,用户身份,并在后台输出"; }
通过SecurityContextHolder获取
SecurityContextHolder:持有的是安全上下文(SecurityContext)的信息。SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息。
@RequestMapping("/main") public String main(){ SecurityContext context = SecurityContextHolder.getContext(); UserDetails details = (UserDetails) context.getAuthentication().getPrincipal(); System.out.println(details.getUsername()); System.out.println(details.getPassword()); System.out.println(details.getAuthorities()); return "这里我获取了用户名,用户密码,用户身份"; }
通过Authentication获取
@RequestMapping("/main") public String main(Authentication authentication){ UserDetails details = (UserDetails) authentication.getPrincipal(); System.out.println(details.getUsername()); System.out.println(details.getPassword()); System.out.println(details.getAuthorities()); return "这里我获取了用户名,用户密码,用户身份"; }
记住我功能(Token)
两种实现方式
- 基于简单加密Token的方式
- 基于持久化Token方式
基于简单加密Token的方式
在之前创建的项目用户登录页login.html中新增一个记住我功能勾选框
<label> <input type="checkbox" name="rememberme"> 记住我 </label>
打开SecurityConfig类,重写configure(HttpSecurity http)方法进行记住我功能配置
//开启记住我功能配置 http.rememberMe() //指定在登录时“记住我”的 HTTP 参数,默认为 remember-me .rememberMeParameter("rememberme") //token有效时间,单位是s .tokenValiditySeconds(200);
基于持久化Token方式
需要在数据库中创建一个存储cookie信息的持续登录用户表
create table persistent_logins ( username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);
打开SecurityConfig类,重写configure(HttpSecurity http)方法进行记住我功能配置
加入.tokenRepository(tokenRepository());
http.rememberMe() .rememberMeParameter("rememberme") .tokenValiditySeconds(200) .tokenRepository(tokenRepository());
写一个配置方法
@Bean public JdbcTokenRepositoryImpl tokenRepository(){ JdbcTokenRepositoryImpl jr=new JdbcTokenRepositoryImpl(); jr.setDataSource(dataSource); return jr; }
重启项目进行效果测试,通过浏览器访问项目首页,输入正确的账户信息,勾选记住我后跳转到项目首页index.html,查看数据库中创建的存储cookie信息的持续登录用户表。
重新打开刚才使用的浏览器,访问项目首页并直接查看影片详情,会发现无需重新登录就可以直接访问。此时,再次查看数据库中表数据信息。
返回到浏览器首页,单击首页上方的用户“注销”连接,在Token有效期内进行用户手动注销。此时,再次查看数据库中表数据信息。
SpringSecurity和thymeleaf整合
整合步骤
- 添加thymeleaf-extras-springsecurity5依赖启动器
- 修改前端页面,使用Security相关标签进行页面控制
- 效果测试
添加thymeleaf-extras-springsecurity5依赖启动器
在上面maven配置文件中我加了这个的
<dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency>
修改前端页面,使用Security相关标签进行页面控制
标签
在index.html页面中引入Security安全标签xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
<h1 align="center">欢迎进入电影网站首页<span style="padding-left: 50px;font-size: 20px"><span sec:authentication="principal.username"></span> <a th:href="@{/user/logout}" href="login/login.html">退出登录</a></span></h1> <hr> <div sec:authorize="hasAnyAuthority('common','vip')"> <!--有 vip 或 common 权限就显示,没有就不显示--> <h3>普通电影</h3> <ul> <li><a th:href="@{/detail/common/1}" href="detail/common/1.html">飞驰人生</a></li> <li><a th:href="@{/detail/common/2}" href="detail/common/2.html">夏洛特烦恼</a></li> </ul> </div> <div sec:authorize="hasAuthority('vip')"> <!--有vip这个权限就显示,没有就不显示--> <h3>VIP专享</h3> <ul> <li><a th:href="@{/detail/vip/1}" href="detail/vip/1.html">速度与激情</a></li> <li><a th:href="@{/detail/vip/2}" href="detail/vip/2.html">猩球崛起</a></li> </ul> </div>
CSRF防护功能
CSRF(Cross-site request forgery):跨站请求伪造
CSRF攻击可以在受害者毫不知情的情况下以受害者的名义伪造恶意请求发送给攻击页面,从而在用户未授权的情况下执行权限保护下的操作,从而造成受害者私密信息泄露以及财产损失。
CSRF攻击原理图
用户点了其他恶意网站B的链接,然后恶意网站B用你的电脑信息给网站A发送信息来操作网站A中你的信息
比如:恶意网站B中有个连接<img src=“localhost:8080/money?name=“张三”&transfer=10000”> ,你点了就加载了这个链接,转账给张三一万元,这只是一个很简单的举例而且
如果你说那用 post 请求就行了啊,那我在恶意网站B写个form表单,改为post请求,然后在js中写启动加载时,就把表单数据提交上去,这样你的信息还是被我篡改了
CSRF防护措施
1.HTTP Referer同源验证
HTTP Referer记录了该 HTTP 请求的来源地址。可以对Referer的值进行判断,如果不是来自于同一个网站,则认为是CSRF攻击。但是有些浏览器可以修改Referer,所以这个也不保险
2.验证码
强制用户必须与应用进行交互,才能完成最终请求。验证码能很好遏制CSRF攻击,但不能作为主要解决方案。这个是最有效的,但是不可能以有个需要修改的都需要用户来输入验证码撒,这样很影响用户体验,所以只在重要的时候使用验证码,比如转账啥的。3.CSRF Token令牌(Spring Security) (这个一般来说是用的最多的)
(1)用户登录后,服务端生成一个Token,放在用户的Session中
(2)在页面表单中附带上Token参数。
(3)用户发送请求都需要携带这个Token参数
CSRF防护功能
整合Spring Security安全框架后,项目默认启用了CSRF安全防护功能,项目中所有涉及到数据修改方式的请求(POST、PUT、DELETE)都会被拦截。
针对这种情况,可以有两种处理方式:
- 直接关闭Security默认开启的CSRF防御功能
- 配置Security需要的CSRF Token
关闭CSRF防护功能
配置类SecurityConfig,在重写的configure(HttpSecurity http)方法中进行关闭配置即可
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); }
开启CSRF防护功能(默认开启,把 http.csrf().disable(); 删掉就行,原理就是给后台传数据的时候,带个Token值给后台来判断)
在请求中配置Security需要的CSRF Token:
- 针对Form表单数据修改的CSRF Token配置
- 针对Ajax数据修改请求的CSRF Token配置
针对Form表单数据修改的CSRF Token配置
在Form表单中加入一个token隐藏域或者使用th:action指定请求地址
<form method="post" action="/updateUser"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> 用户名: <input type="text" name="username" /> <br /> 密 码: <input type="password" name="password" /> <br /> <button type="submit">修改</button> </form>
针对Ajax数据修改请求的CSRF Token配置
1、在页面<head>
标签中添加<meta>
子标签,并配置CSRF Token信息
<head> <meta name="_csrf" th:content="${_csrf.token}"/> <meta name="_csrf_header" th:content="${_csrf.headerName}"/> </head>
2、在具体的Ajax请求中获取<meta>
子标签中设置的CSRF Token信息并绑定在HTTP请求头中进行请求验证
$(function () { var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); $(document).ajaxSend(function(e, xhr, options) { xhr.setRequestHeader(header, token); }); });
转自:https://blog.csdn.net/qq_57581439/article/details/125182300