内置访问控制方法
Spring Security 匹配了 URL 后调用了permitAll()
表示不需要认证,随意访问。在 Spring Security 中提供了多种内置控制。
permitAll()
permitAll()表示所匹配的 URL 任何人都允许访问。
authenticated()
authenticated()表示所匹配的 URL 都需要被认证才能访问。
anonymous()
anonymous()表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为 anonymous()的 url 会执行 filter 链中
denyAll()
denyAll()表示所匹配的 URL 都不允许被访问。
rememberMe()
被“remember me”的用户允许访问
fullyAuthenticated()
如果用户不是被 remember me 的,才可以访问。
角色权限判断
除了之前讲解的内置权限控制。Spring Security 中还支持很多其他权限控制。这些方法一般都用于用户已经被认证后,判断用户是否具有特定的要求。
hasAuthority(String)
判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建 User 对象时指定的。下图中 admin和normal 就是用户的权限。admin和normal 严格区分大小写。
在配置类中通过 hasAuthority(“admin”)设置具有 admin 权限时才能访问。
.antMatchers("/main1.html").hasAuthority("admin")
hasAnyAuthority(String ...)
如果用户具备给定权限中某一个,就允许访问。
下面代码中由于大小写和用户的权限不相同,所以用户无权访问
.antMatchers("/main1.html").hasAnyAuthority("adMin","admiN")
hasRole(String)
如果用户具备给定角色就允许访问。否则出现 403。
参数取值来源于自定义登录逻辑 UserDetailsService
实现类中创建 User 对象时给 User 赋予的授权。
在给用户赋予角色时角色需要以:ROLE_开头
,后面添加角色名称。例如:ROLE_abc 其中 abc 是角色名,ROLE_是固定的字符开头。
使用 hasRole()时参数也只写 abc 即可。否则启动报错。
给用户赋予角色:
在配置类中直接写 abc 即可。
.antMatchers("/main1.html").hasRole("abc")
hasAnyRole(String ...)
如果用户具备给定角色的任意一个,就允许被访问
hasIpAddress(String)
如果请求是指定的 IP 就运行访问。
可以通过 request.getRemoteAddr()
获取 ip 地址。
需要注意的是在本机进行测试时 localhost 和 127.0.0.1 输出的 ip地址是不一样的。
当浏览器中通过 localhost 进行访问时控制台打印的内容:
当浏览器中通过 127.0.0.1 访问时控制台打印的内容:
当浏览器中通过具体 ip 进行访问时控制台打印内容:
.antMatchers("/main1.html").hasIpAddress("127.0.0.1")
自定义403处理方案
使用 Spring Security 时经常会看见 403(无权限),默认情况下显示的效果如下:
而在实际项目中可能都是一个异步请求,显示上述效果对于用户就不是特别友好了。Spring Security 支持自定义权限受限。
新建类
新建类实现 AccessDeniedHandler
package com.yjxxt.springsecuritydemo.handler; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @Component public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setHeader("Content-Type", "application/json;charset=utf-8"); PrintWriter out = response.getWriter(); out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}"); out.flush(); out.close(); } }
修改配置类
配置类中重点添加异常处理器。设置访问受限后交给哪个对象进行处理。
myAccessDeniedHandler 是在配置类中进行自动注入的。
//异常处理 http.exceptionHandling() .accessDeniedHandler(myAccessDeniedHandler);
基于表达式的访问控制
access()方法使用
之前学习的登录用户权限判断实际上底层实现都是调用access(表达式)
可以通过access()
实现和之前学习的权限控制完成相同的功能。
以 hasRole 和 和 permitAll 举例
使用自定义方法
虽然这里面已经包含了很多的表达式(方法)但是在实际项目中很有可能出现需要自己自定义逻辑的情况。
判断登录用户是否具有访问当前 URL 权限。
新建接口及实现类
MyService.java
package com.yjxxt.springsecuritydemo.service; import org.springframework.security.core.Authentication; import javax.servlet.http.HttpServletRequest; public interface MyService { boolean hasPermission(HttpServletRequest request, Authentication authentication); }
MyServiceImpl.java
package com.yjxxt.springsecuritydemo.service.impl; import com.yjxxt.springsecuritydemo.service.MyService; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.Collection; @Component public class MyServiceImpl implements MyService { @Override public boolean hasPermission(HttpServletRequest request, Authentication authentication) { Object obj = authentication.getPrincipal(); if (obj instanceof UserDetails){ UserDetails userDetails = (UserDetails) obj; Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities(); return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI())); } return false; } }
修改配置类
在 access 中通过@bean的id名.方法(参数)的形式进行调用配置类中修改如下:
//url拦截 http.authorizeRequests() //login.html不需要被认证 // .antMatchers("/login.html").permitAll() .antMatchers("/login.html").access("permitAll") // .antMatchers("/main.html").hasRole("abc") .antMatchers("/main.html").access("hasRole('abc')") .anyRequest().access("@myServiceImpl.hasPermission(request,authentication)")
基于注解的访问控制
在 Spring Security 中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过@EnableGlobalMethodSecurity
进行开启后使用。
如果设置的条件允许,程序正常执行。如果不允许会报 500
这些注解可以写到 Service 接口或方法上,也可以写到 Controller或 Controller 的方法上。通常情况下都是写在控制器方法上的,控制接口URL是否允许被访问。
@Secured
@Secured 是专门用于判断是否具有角色的。能写在方法或类上。参数要以 ROLE_开头。
开启注解
在 启 动 类 ( 也 可 以 在 配 置 类 等 能 够 扫 描 的 类 上 ) 上 添 加@EnableGlobalMethodSecurity(securedEnabled = true)
@SpringBootApplication @EnableGlobalMethodSecurity(securedEnabled = true) public class SpringsecurityDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringsecurityDemoApplication.class, args); } }
在控制器方法上添加@Secured 注解
/** * 成功后跳转页面 * @return */ @Secured("ROLE_abc") @RequestMapping("/toMain") public String toMain(){ return "redirect:/main.html"; }
配置类
@Override protected void configure(HttpSecurity http) throws Exception { //表单提交 http.formLogin() //自定义登录页面 .loginPage("/login.html") //当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl .loginProcessingUrl("/login") //登录成功后跳转页面,POST请求 .successForwardUrl("/toMain") //url拦截 http.authorizeRequests() //login.html不需要被认证 .antMatchers("/login.html").permitAll() //所有请求都必须被认证,必须登录后被访问 .anyRequest().authenticated(); //关闭csrf防护 http.csrf().disable(); }
@PreAuthorize/@PostAuthorize
@PreAuthorize 和@PostAuthorize 都是方法或类级别注解。
@PreAuthorize
表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和access()方法参数取值相同,都是权限表达式。@PostAuthorize
表示方法或类执行结束后判断权限,此注解很少被使用到。
开启注解
@SpringBootApplication @EnableGlobalMethodSecurity(prePostEnabled = true) public class SpringsecurityDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringsecurityDemoApplication.class, args); } }
添加@PreAuthorize
在控制器方法上添加@PreAuthorize,参数可以是任何 access()支持的表达式
/** * 成功后跳转页面 * @return */ @PreAuthorize("hasRole('ROLE_abc')") @RequestMapping("/toMain") public String toMain(){ return "redirect:/main.html"; }
RememberMe功能实现
Spring Security 中 Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,取值为true。Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问
添加依赖
Spring Security 实 现 Remember Me 功 能 时 底 层 实 现 依 赖Spring-JDBC,所以需要导入 Spring-JDBC。以后多使用 MyBatis 框架而很少直接导入 spring-jdbc,所以此处导入 mybatis 启动器同时还需要添加 MySQL 驱动
<!-- mybatis 依赖 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <!-- mysql 数据库依赖 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.18</version> </dependency>
配置数据源
在 application.properties 中配置数据源。请确保数据库中已经存在shop数据库
spring.datasource.driver-class-name= com.mysql.cj.jdbc.Driver spring.datasource.url= jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai spring.datasource.username= root spring.datasource.password= root
编写配置
RememberMeConfig.java
package com.yjxxt.springsecuritydemo.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import javax.sql.DataSource; @Configuration public class RememberMeConfig { @Autowired private DataSource dataSource; @Bean public PersistentTokenRepository getPersistentTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); //自动建表,第一次启动时需要,第二次启动时注释掉 jdbcTokenRepository.setCreateTableOnStartup(true); return jdbcTokenRepository; } }
修改SecurityConfig.java
在SecurityConfig中添加RememberMeConfig和UserDetailsService实现类对象,并自动注入。
在 configure 中添加下面配置内容。
http.rememberMe() //登录逻辑交给哪个对象 .userDetailsService(userService) // 持久层对象 .tokenRepository(persistentTokenRepository);
在客户端页面添加复选框
在客户端登录页面中添加 remember-me 的复选框,只要用户勾选了复选框下次就不需要进行登录了。
<form action="/login" method="post"> 用户名:<input type="text" name="username" /><br/> 密码:<input type="password" name="password" /><br/> <input type="checkbox" name="remember-me" value="true"/><br/> <input type="submit" value="登录" /> </form>
有效时间
默认情况下重启项目后登录状态失效了。但是可以通过设置状态有效时间,即使项目重新启动下次也可以正常登录。
http.rememberMe() //失效时间,单位秒 .tokenValiditySeconds(120) //登录逻辑交给哪个对象 .userDetailsService(userService) // 持久层对象 .tokenRepository(persistentTokenRepository);
Thymeleaf中SpringSecurity的使用
Spring Security 可以在一些视图技术中进行控制显示效果。例如:JSP
或 Thymeleaf
。在非前后端分离且使用 Spring Boot 的项目中多使用 Thymeleaf
作为视图展示技术。
Thymeleaf 对 Spring Security 的 支 持 都 放 在thymeleaf-extras-springsecurityX
中,目前最新版本为 5。所以需要在项目中添加此 jar 包的依赖和 thymeleaf 的依赖。。
<!--thymeleaf springsecurity5 依赖--> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency> <!--thymeleaf依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
在 html 页面中引入 thymeleaf 命名空间和 security 命名空间
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
获取属性
可以在html页面中通过sec:authentication=""
获取
UsernamePasswordAuthenticationToken
中所有 getXXX
的内容,包含父类中的 getXXX
的内容。
根据源码得出下面属性:
name
:登录账号名称principal
:登录主体,在自定义登录逻辑中是 UserDetailscredentials
:凭证authorities
:权限和角色details
:实际上是WebAuthenticationDetails
的实例。可以获取remoteAddress
(客户端 ip)和sessionId
(当前 sessionId)
新建demo.html
在项目 resources 中新建 templates 文件夹,在 templates 中新建demo.html 页面
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"> <head> <title>Title</title> </head> <body> 登录账号:<span sec:authentication="name"></span><br/> 登录账号:<span sec:authentication="principal.username"></span><br/> 凭证:<span sec:authentication="credentials"></span><br/> 权限和角色:<span sec:authentication="authorities"></span><br/> 客户端地址:<span sec:authentication="details.remoteAddress"></span><br/> sessionId:<span sec:authentication="details.sessionId"></span><br/> </body> </html>
编写Controller
thymeleaf 页面需要控制转发,在控制器类中编写下面方法
@RequestMapping("/demo") public String demo(){ return "demo"; }
权限判断
设置用户角色和权限
设定用户具有 admin,/insert,/delete 权限 ROLE_abc 角色。
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_abc,/insert,/delete"));
控制页面显示效果
在页面中根据用户权限和角色判断页面中显示的内容
通过权限判断: <button sec:authorize="hasAuthority('/insert')">新增</button> <button sec:authorize="hasAuthority('/delete')">删除</button> <button sec:authorize="hasAuthority('/update')">修改</button> <button sec:authorize="hasAuthority('/select')">查看</button> <br/> 通过角色判断: <button sec:authorize="hasRole('abc')">新增</button> <button sec:authorize="hasRole('abc')">删除</button> <button sec:authorize="hasRole('abc')">修改</button> <button sec:authorize="hasRole('abc')">查看</button>
退出登录
用户只需要向 Spring Security 项目中发送/logout
退出请求即可。
退出登录
实现退出非常简单,只要在页面中添加/logout
的超链接即可。
<a href="/logout">退出登录</a>
为了实现更好的效果,通常添加退出的配置。默认的退出 url 为/logout
,退出成功后跳转到/login?logout
如果不希望使用默认值,可以通过下面的方法进行修改。
//退出登录 http.logout() //退出登录url .logoutUrl("/logout") //退出登录成功跳转的url· .logoutSuccessUrl("/login.html");
logout其他常用配置源码解读
addLogoutHandler(LogoutHandler)
默认是 contextLogoutHandler
默认实例内容
clearAuthentication(boolean)
是否清除认证状态,默认为 true
invalidateHttpSession(boolean)
是否销毁 HttpSession 对象,默认为 true
logoutSuccessHandler(LogoutSuccessHandler)
退出成功处理器
也可以自己进行定义退出成功处理器。只要实现了LogoutSuccessHandler
接口。与之前讲解的登录成功处理器和登录失败处理器极其类似。
SpringSecurity中的CSRF
从刚开始学习Spring Security时,在配置类中一直存在这样一行代码:http.csrf().disable();
如果没有这行代码导致用户无法被认证。这行代码的含义是:关闭 csrf 防护。
什么是CSRF
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack” 或者 Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议,ip 地址,端口中任何一个不相同就是跨域请求。
客户端与服务进行交互时,由于 http 协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id 可能被第三方恶意劫持,通过这个 session id 向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。
2、Spring Security中的CSRF
从 Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf
值为token(token 在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。
2.1、编写控制器方法
编写控制器方法,跳转到 templates 中 login.html 页面。
@RequestMapping("/showLogin") public String showLogin(){ return "login"; }
2.2、新建login.html
红色部分是必须存在的否则无法正常登录。
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" > <head> <title>Title</title> </head> <body> <form action="/login" method="post"> <input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/> 用户名:<input type="text" name="username" /><br/> 密码:<input type="password" name="password" /><br/> <input type="submit" value="登录" /> </form> </body> </html>
修改配置类
在配置类中注释掉 CSRF 防护失效
//关闭csrf防护 // http.csrf().disable();