当一个系统建立后,通常都有一些权限控制,使得不同的角色有不同的权限操作,下面就针对web应用中常用的自定义用户授权进行介绍
在实际的开发中,网站访问多是基于HTTP请求的,在前一个学习笔记中,我们已经讨论过重写WenSecurityConfigurerAdapter类的configure(HttpSecurity http)方法可以对基于HTTP的请求访问进行控制。下面问 通过对configure(HttpSecurity)方法的讲解,分析自定义用户访问控制的实现流程
configure(HttpSecurity)方法的参数类型是HttpSecurity类,HttpSecurity类提供给了请求的限制以及权限、Session管理配置、CSRF跨站请求问题等方法 下面是HttpSecurity类的主要方法及说明
方法描述authorizeRequests()开启基于HttpServletRequest请求访问的限制formLogin()开启基于表单的用户登录httpBasic()开启基于HTTP请求的Basic认证登录logout()开启退出登录的支持sessionManagement()开启Session管理配置rememberMe()开启记住我功能csrf()配置CSRF跨站请求伪造防护功能此处重点讲解用户访问控制,这里先对authorizeRequests()方法的返回值做进一步查看,其中涉及用户访问控制的主要方法如下
方法描述antMatchers(java.lang.String…antPatterns)开启Ant风格的路径匹配mvcMatchers(java.lang.String…patterns)开启MVC风格的路径匹配(与Ant风格类似)regesMatchers(java.lang.String…regexPatterns)开启正则表达式的路径匹配and()功能连接符anyRequest()匹配任何请求rememberMe()开启记住我功能access(String attribute)匹配给定的SpEL表达式计算结果是否为truehasAnyRole(String…role)匹配用户是否有参数中的任意角色hasRole(String role)匹配用户是否有某一个角色hasAnyAuthority(String…authorities)匹配用户是否有参数值任意权限hasAuthority(String authorities)匹配用户是否有某一个权限authenticated()匹配已经登录认证的用户fullyAuthenticated()匹配完整登录认证的用户(非rememberMe登录用户)haslpAddress(String ipaddressExpression)匹配某IP地址的访问请求permitAll()无条件对请求进行放行下面在自定义yoghurt认证案例的基础上,配置用户访问控制,演示Security授权管理的用法
自定义用户访问控制 打开之前创建的MVC Security自定义配置类SecurityConfig,重写configure(Http Securityhttp)方法进行用户访问控制 /** *config()方法设置了用户访问权限,其中路径为"/"的请求直接放行 * 路径为"/detail/common/**"的请求,只有common(即ROLE_common权限)才允许访问 * vip同上 * 其他请求则要求用户必须先进行登录认证 */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/").permitAll() .antMatchers("/detail/common/**").hasRole("common") .antMatchers("/detail/vip/**").hasRole("vip") .anyRequest().authenticated().and().formLogin(); } 效果测试 这里我们先点击普通用户的电影 当前示例没有配置完善的注销功能,所以要切换yoghurt的话需要重启浏览器,再登录即可在前面的案例中可以看到,我们没有配置用户登录界面和登录处理方法,但是却有默认的界面和处理方法,这是Spring Security提供的。实际开发中,通常会要求定制更美观的登录页面和配置更好的错误提示,下面我们就围绕formLogin()方法来讨论自定义用户登录的具体方法
方法描述loginPage(String loginPage)用户登录页面跳转辣眼睛,默认为get请求的/loginsuccessForwardUrl(String forwardUrl)用户登录成功后的重定向地址successHandler(AuthenticationSuccessHandler successHandler)用户登录成功后的处理defaultSuccessUrl(String defaultSuccess)用户直接登录后默认跳转地址failureForwardUrl(String forwardUrl)用户登录失败后的重定向地址failureUrl(String autnenticationUrl)用户登录失败后的跳转地址,默认为/login?errorfailureHandler(AuthenticationFailureHandler autnenticationFailureHandler)用户登录失败后的错误处理usernameParameter(String usernameParameter)登录用户的用户名参数,默认为usernamepasswordParameter(String passwordParameter)登录用户的密码参数,默认为passwordloginProcessingUrl(String loginProcessingUrl)登录表单提交的路径,默认为post请求的/loginpermitAll()无条件对请求放行下面我们在签约一个自定义用户访问控制案例的基础上实现自定义用户登录功能
自定义用户登录界面 在resources/templates下创建login/login.html(login文件夹专门处理用户请求) <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>用户登录界面</title> <link th:href="@{/login/css/bootstrap.min.css}" rel="stylesheet"> <link th:href="@{/login/css/signin.css}" rel="stylesheet"> </head> <body class="text-center"> <form class="form-signin" th:action="@{/userLogin}" th:method="post" > <img class="mb-4" th:src="@{/login/img/login.jpg}" width="72px" height="72px"> <h1 class="h3 mb-3 font-weight-normal">请登录</h1> <!-- 用户登录错误信息提示框 --> <div th:if="${param.error}" style="color: red;height: 40px;text-align: left;font-size: 1.1em"> <img th:src="@{/login/img/loginError.jpg}" width="20px">用户名或密码错误,请重新登录! </div> <input type="text" name="name" class="form-control" placeholder="用户名" required="" autofocus=""> <input type="password" name="pwd" class="form-control" placeholder="密码" required=""> <div class="checkbox mb-3"> <label> <input type="checkbox" name="rememberme"> 记住我 </label> </div> <button class="btn btn-lg btn-primary btn-block" type="submit" >登录</button> <p class="mt-5 mb-3 text-muted">Copyright© 2019-2020</p> </form> </body> </html>在上面代码中,我们在<from>中用post方式通过“/userLogin”提交请求,表单中用户名参数,密码参数和提交路径均可自定义;我们使用th:if="${param.error}"来判断请求中是否带error参数,从而判断是否登录成功,该参数时security默认的,用户可自定义
我们还要在static/login下引入css样式和img来渲染页面 2. 自定义用户登录跳转 在之前创建的FilmeController中添加一个跳转到登录页面的方法
@GetMapping("/userLogin") public String toLoginPage(){ return "login/login"; }Security默认采用get方式的“/login”请求用于向登录页面跳转,使用post方式的"/login"请求用于对登录后的数据处理
自定义用户登录控制 重写SecurityConfig.config(HttpSecurity http) @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/").permitAll() //需要对static文件夹下的静态文件统一放行 .antMatchers("/login/**").permitAll() .antMatchers("/detail/common/**").hasRole("common") .antMatchers("/detail/vip/**").hasRole("vip") .anyRequest().authenticated().and().formLogin(); //自定义用户登录控制 http.formLogin().loginPage("/userLogin").permitAll() .usernameParameter("name").passwordParameter("pwd") .defaultSuccessUrl("/") .failureUrl("/userLogin?error"); }关于formLogin()方法具体介绍如下:
loginPage("/userLogin")指定了向自定义登录页跳转的请求路径(前面定义的toLoginPage()的请求映射路径),并使用permitAll()对进行登录跳转的请求放行usernamerameter("name")和passwordParameter("pwd")用于接收登录时提交的用户名和密码,其中的name和pwd必须和login.html中的属性值保持一致,如果属性值是默认的username和password,则这两个方法可以省略defaultSuccessUrl("/")指定了用户登录成功后默认跳转到首页面failureUrl("/userLogin?error")用来控制用户登录认证失败后的跳转路径,该方法默认参数是“/login?error”。其中,参数的“/userLogin”为向登录页面跳转的映射,error是一个错误标识,作用是登录失败后在登录页面进行接收判断,必须和login.html中的${param.error}保持一致 效果测试 >如果运行项目点击资源出现500错误,同时控制台报template might not exist or might not be accessible by any of the configured错误,则是因为Spring Boot没有解析出静态资源文件位置,我们只需在pom文件的<build>中加入<!-- 配置将哪些资源文件(静态文件/模板文件/mapper文件)加载到tomcat输出目录里 --> <resources> <resource> <directory>src/main/java</directory><!--java文件的路径--> <includes> <include>**/*.*</include> </includes> <!-- <filtering>false</filtering>--> </resource> <resource> <directory>src/main/resources</directory><!--资源文件的路径--> <includes> <include>**/*.*</include> </includes> <!-- <filtering>false</filtering>--> </resource> </resources>自定义用户退出主要考虑退出后的会话如何管理以及跳转到哪个页面。GttpSecurity类的logout()方法就是用于处理用户退出的,它默认处理路径为“/logout”的post类型请求,同时也会清楚Session和“Remember Me(记住我)”等任何默认用户配置,下面我们就来讨论以下关于logout()及其实现吧 logout()方法设计用户退出的主要方法如下:
方法描述logoutUrl(String logoutUrl)用户退出处理控制Url,默认为post请求的/logoutlogoutSuccessUrl(String logoutSuccessUrl)用户退出成功后的重定向地址logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler)用户退出后的处理器设置deleteCookies(String…cookiesNamesToClear)用户退出后删除指定CookiesinvalidateHttpSession(boolean invalidateHttpSession)用户退出后是否立即清楚Session(默认为true)ClearAutnenticarion(boolean clearAutnentication)用户退出后是否立即清楚Autnentication用户认证信息(默认为true)下面我们继续在前面的案例基础上继续实现自定义用户退出功能吧
添加自定义用户退出连接 我们在index.html页面上新增一个用户退出按钮 <form th:action="@{/mylogout}" method="post"> <input th:type="submit" th:value="注销" /> </form>需要说明的是,我们引入Spring Security后会自动开启CDRF防护功能(即跨站请求伪造防护),用户退出时必须使用post请求;如果关闭了CSRF防护功能,则可以使用任意方式的HTTP请求进行用户注销
自定义用户退出控制 定义好用户退出连接后,无需再Controller中额外定义用户退出方法,可以直接再Security中定制logout()方法实现用户退出 SecurityConfig.configure(HttpSecurity http) @Override protected void configure(HttpSecurity http) throws Exception { //自定义用户退出控制 http.logout() //用户退出的指定路径必须和index.xml中退出表单的action保持一致,如果表单使用默认的“/logout”,则该方法可以省略 .logoutUrl("/mylogout") //退出成功后返回首页 .logoutSuccessUrl("/"); } 效果测试(略,当我们注销后想再访问影片详情,会跳转到登录界面,说明我们退出登录功能成功实现)在传统项目中进行用户登录处理时,通常会查询用户是否存在,存在则登录成功,同时将当前用户放在Session中;前面案例中,进行用户授权管理后并没有显示登录后的用户处理情况,那这种情况下登录后的用户存放在哪?存储的用户数据及结构又是怎样?下面我们通过HttpSession和SecurityContextHolder两种方式来获取登录后的用户信息
使用HttpSession获取用户信息 为了简化操作,在之前创建的FilmeController控制类中新增一个用于获取当前会话用户信息的getUser()方法 @GetMapping("/getuserBySession") @ResponseBody public void getUser(HttpSession session){ //从当前HttpSession获取绑定到此会话的所有对象名称 Enumeration<String> names = session.getAttributeNames(); while(names.hasMoreElements()){ //获取HttpSession中会话名称 String element = names.nextElement(); //获取HttpSession中的应用上下文 SecurityContextImpl attribute = (SecurityContextImpl) session.getAttribute(element); System.out.println("element=="+element); System.out.println("attribute=="+attribute); //获取用户相关信息 Authentication authentication = attribute.getAuthentication(); UserDetails principal = (UserDetails) authentication.getPrincipal(); System.out.println("principal=="+principal); System.out.println("username=="+principal.getUsername()); } }在上述代码中,在getUser(HttpSession session)方法中通过获取当前HttpSesseion的相关方法遍历获取会话中的用户信息。getAttribute(element)获取会话对象时,默认返回一个Object对象,其本质是一个SecurityContextImpl,为了方便查看对象数据,所以强制转换成SecurityContextImpl;在获取认证用户信息时,使用Authentication的getPrincipal()方法,默认返回的也是一个Object对象,其本质是封装用户信息的UserDetails封装类,其中包括用户名、密码、权限、是否过期等
接下来我们以debug模式启动项目,然后访问项目首页,随便查看一个影片详情并登录,然后在统一浏览器执行“http://localhost:8080/getyserBySession”来获取用户详情
当前HttpSession会话中只有一个key为“SPRING_SECURITY_CONTEXT”的用户信息,并且用户信息被封装在SecurityContextImpl类对象中;另外通过SecurityContextImpl类的相关方法可以进一步获取到当前登录用户的更多信息,其中关于用户的主要信息都封装在UserDetails类中
使用SecurityContextHolder获取用户信息 Spring Security针对拦截的登录用户专门提供了一个SecurityContextHolder类来获取Spring Security的应用上下文SecurityContext,进而获取封装的用户信息 在FilmeController控制类中新增一个获取当前会话用户信息的getUser2() @GetMapping("/getuserByContext") @ResponseBody public void getUser2(){ //获取应用上下文 SecurityContext context = SecurityContextHolder.getContext(); System.out.println("userDetails=="+context); //获取用户相关信息 Authentication authentication = context.getAuthentication(); UserDetails principal = (UserDetails) authentication.getPrincipal(); System.out.println(principal); System.out.println("username=="+principal.getUsername()); }通过与HttpSession方式获取用户信息的示例对比可以发现,两种方法的区别就是获取SecurityContext不同,其他后续方法基本一致;HttpSession方式比较传统,必须映入HttpSession对下个,而SecurityContextHolder相对简单,也是比较推荐的方法
我们在浏览网页时登录常常有记住我的功能,勾选之后可以在一段时间内免去重复的登录操作,继续访问目标网站,下面我们就围绕HttpSecurity类的主要方法rememberMe()方法来讨论记住我功能的具体实现
rememberMe()相关设计及主要方法如下
方法描述rememberMeParameter(String rememberMeParameter)指示在登录时记住用户的HTTP参数key(String key)记住我认证生成的Token令牌标识tokenValidityRepository(int tokenValiditySeconds)记住我Token令牌有效期,单位为s(秒)tokenRepository(PersistentTokenRepository tokenRepository指定要要使用的PersistentTokenRepository,用来配置持久化Token令牌alwaysRemember(boolean alwaysRemember)是否应该始终创建记住我Cookie,默认falseclearAuthentication(boolean clearAuthentication)是否设置Cookie为安全,如果设置为true,则必须通过HTTPS进行连接请求Spring Security有两种记住我的实现:
简单的使用加密来保证基于Cookie中Token的安全通过数据库或其他持久化机制来保存生成的Tolen 基于简单加密的Token方式 当用户选中记住我并成功登录后,Spring Security将生成一个Cookie并发送给客户端浏览器。其中Cookie值由下列方式加密而成功 base64(username+":"+expirationTime+":" +md5Hex(username+":"+expirationTime+":"+password+":"+key)) username:登录的用户名password:登录用户密码expirationTime:记住我中Token的失效日期,以毫秒为单位key:防止修改Token的标识基于简单加密Token方式中Token在指定的时间内有效,且必须保证Token中所包含的username、password和key没有被改变;但这种方式页存在隐患,任何人获取到该记住我功能的Token后,都可以在该Token过期前进行登录,只有当用户觉察到Token被盗用后,才会对自己的登录密码进行修改来立即使其原有的记住我Token失效 下面我们就简单的来是实现一个记住我功能,首先在login.html中新增记住我选项
<div class="checkbox mb-3"> <label> <!--Security提供的记住我功能的name属性值默认为“remember-me”--> <input type="checkbox" name="rememberme"> 记住我 </label> </div>重写SecurityConfig类的configure(HttpSecurity http)方法进行记住我功能配置
@EnableWebSecurity//开去NVC Security安全支持 public class SecurityConfig extends WebSecurityConfigurerAdapter { /** *config()方法设置了用户访问权限,其中路径为"/"的请求直接放行 * 路径为"/detail/common/**"的请求,只有common(即ROLE_common权限)才允许访问 * vip同上 * 其他请求则要求用户必须先进行登陆认证 */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/").permitAll() //需要对static文件夹下的静态文件统一放行 .antMatchers("/login/**").permitAll() .antMatchers("/detail/common/**").hasRole("common") .antMatchers("/detail/vip/**").hasRole("vip") .anyRequest().authenticated().and().formLogin(); //自定义用户登陆控制 http.formLogin() .loginPage("/userLogin").permitAll() .usernameParameter("name").passwordParameter("pwd") .defaultSuccessUrl("/") .failureUrl("/userLogin?error"); //自定义用户退出控制 http.logout() //用户退出的指定路径必须和index.xml中退出表单的action保持一致,如果表单使用默认的“/logout”,则该方法可以省略 .logoutUrl("/mylogout") //退出成功后返回首页 .logoutSuccessUrl("/"); //定制remember-me 记住我功能 http.rememberMe() .rememberMeParameter("rememberme") .tokenValiditySeconds(200); } }然后关闭浏览器,再重新访问项目,可以直接查看影片详情(略)
基于持久化Token与简单加密的Token在实现功能上大体相同,都是在用户选中记住我并成功登录后,将生成的Token存入Cookie中并发送到客户端浏览器,下次用户通过同一客户端访问系统时,系统将直接从客户端Cookie中读取Token进行认证
但两者主要区别在于: 简单加密的Token生成的Token将在客户端保存一段时间,只要用户不退出登录或不修改密码,在Cookie失效前,任何人都可以无限制使用Token进行登录 而持久化Token实现逻辑如下
用户选中记住我并成功登录后,Security会把username、随机产生的序列号和生成的Token进行持久化存储(如一个数据表中),同时将他们的组合生成一个Cookie发送给浏览器客户端但用户再次访问系统,先检查客户端携带的Cookie是否对应Cookie中所包含的username、序列号和Token是都和数据库中的一致,是则通过登录,这是系统重新再生成一个新的Token去代替数据库中旧的Token,并将新的Cookie再发送给客户端反之不匹配,则可能Cookie被盗用,由于盗用者的使用,导致其也会生成一个新的Token,所以用户登录时变回发生Token不匹配,需要重新登录,并生成新的Cookie和Token。同时Security发现盗用情况,,它会删除数据库中与用户相关的Token记录,这样盗用者便不能使用最原始的Cookie继续登录啦如果用户访问系统没有携带Cookie,或者包含的username和序列化与数据库中保存的不一致,便需要重新登录综上所述,持久化Token避免了简单加密Token的缺点,用户可以及时发现异常,但也给但用者留下了在用户进行第二次登陆前进行恶意操作的机会,只有在用户进行第二次登录并更新Token和Cookie时才会避免这种问题。但总的来说,还是推荐使用持久化Token。 下面我们就来实现以下持久化Token。为了对持久化Token进行存储,我们需要在数据库中创建一个存储Cookie信息的持续登录用户表persistent_logins
# 记住我功能中创建持久化Token存储的数据表 create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);上面的series标识存储随机生成的序列号,last_used标识最近登录日期;需要注意的是,在默认情况下基于持久化Token会使用上述官方提供的用户表persistent_logins进行持久化Token的管理,我们不需要自定义存储Cookie信息的用户表,你也可以自行查询一下相关方法
重写SecurityConfig类的configure(HttpSecurity http)方法
@Override protected void configure(HttpSecurity http) throws Exception { //其他方法省略 //定制remember-me 记住我功能 http.rememberMe() //如果页面中使用了默认的“remember-me”,则该方法可以省略 .rememberMeParameter("rememberme") //设置Token有效期为200s .tokenValiditySeconds(200) //对Cookie信息进行持久化管理 .tokenRepository(tokenRepository()); } //持久化Token存储 @Bean public JdbcTokenRepositoryImpl tokenRepository(){ JdbcTokenRepositoryImpl jr = new JdbcTokenRepositoryImpl(); jr.setDataSource(dataSource); return jr; } }与简单加密Token相比,持久化Token加入了tokenRepository(tokenRepository())方法对Cookie信息进行持久化管理。tokenRepository()参数会返回一个设置dataSource数据源的JdbcTokenRepositoryImpl实现类对象,该对象包含操作Token的各种方法
启动项目,勾选记住我并登录来到index页面,然后到数据库查看persistent_logins表数据信息 然后我们关闭浏览器,再打开重新访问 如果我们注销用户
CSRF(Cross-site request forgery,跨站请求伪造),也被称为“One Click Attack”(一键攻击)或“Session Riding”(会话控制),通常缩写为CSRF或XSRF,是一种网站恶意攻击。与传统XSS攻击(Cross-site Scripting,跨站脚本攻击)相比,CSRF更加难防,更危险。CSRF可以在用户不知情的情况下,以其名义伪造请求发送给攻击页面,从而在用户未授权情况下执行在权限保护下的操作 例如用户tiaotiao登录英航站点服务器准备转账操作,期间,tiaotiao不小心查看了一个黑客恶意网站,该网站便获取到tiaotiao登录后的浏览器与银行间尚未过期的Session信息,而tiaotiao浏览器的Cookie包含tiaotiao银行账户的认证信息,这时黑客便伪装成条条认证后的合法用户对银行账户进行操作
在讨论如何低于CSRF攻击的例子前,先明确CSRF攻击的对象,,也就是要保护的对象。在上面的例子中可知,CSRF攻击是黑客借助用户的Cookie片区服务器信任,但是黑客并不能获取Cookie,也不知其具体内容。另外,对于服务器返回的结果,由于浏览器同源策略的限制,黑客无法进行解析。所以黑客能做的便是伪造正常用户给服务器发送请求,以执行请求中所描述的命令,在服务器端直接改变数据的值,而非窃取服务器中的数据。因此,针对CSRF攻击所要保护的对象是那些可以直接产生数据变化的服务,而对于读取数据的服务,可以不进行CSRF保护。 例如银行转账操作会高边账号金额,需要进行CSRF保护。获取银行卡等级信息是读取操作,不会改变数据,可以不保护。 目前业界防御CSRF攻击主要由一下3中策略
验证HTTP Referer字段在请求地址中添加Token并验证在HTTP头自定义属性并验证 Spring Security提供了CSRF防御相关方法如下: 方法描述disable()关闭Security默认开启的CSRF防御功能csrfTokenRepository(CsrfTokenRepository csrfTokenRepository)指定要使用的CsrfTokenRepository(Token令牌持久化仓库)。默认是由LazyCsrfTokenRepository包装的HttpSessionCsrfTokenRepositoryrequireCsrfProtectionMatcher(RequireCsrfProtectionMatcher requireCsrfProtectionMatcher)指定针对什么类型的请求应用CSRF防护功能。默认设置是忽略GET、HEAD、TRACE和OPTIONS请求,而处理并防御其他所有请求结合上面的方法我们来对CSRF防护功能进行说明
CSRF防护功能关闭 Security默认开启CSRF,并对PATCH、POST、PUT和DELETE等数据修改方法进行认证才可正常访问,下面编写一个页面进行演示说明 创建数据修改页面(在resources/templates下创建csrf.csrfText.html模拟修改用户账号信息) <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>用户修改</title> </head> <body> <div align="center"> <form method="post" th: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> </div> </body> </html>编写后台控制层方法(在controller包下创建控制类CSRFController)
@Controller public class CSRFController { @GetMapping("/toUpdate") public String toUpdate(){//向用户修改页跳转 return "csrf/csrfTest"; } /** *知识演示了获取的请求参数,没有具体的业务实现 */ @ResponseBody @PostMapping("/updateUser") public String updateUser(@RequestParam String username, @RequestParam String password, HttpServletRequest request){//用户修改提交处理 System.out.println(username); System.out.println(password); String csrf_token = request.getParameter("_csrf"); System.out.println(csrf_token); return "ok"; } }CSRF默认防护效果测试 我们访问项目“localhost:8080/toUpdate”,由于security被拦截跳转至登录页面,我们登录后,页面便跳转到用户修改页面
点击修改按钮后,页面会变成403和Forbidden(禁止)的错误提示信息,而后台页没有任何响应。这说明启用了默认的CSRF安全防护功能,而上述被拦截的本质原因便是数据修改请求中没有携带CSRF Token(CSRF令牌)相关参数信息,所以被认为是不安全的请求
在上面例子中,所有涉及数据修改的请求都会被拦截,对于这种情况想关闭的话,第一种方法是直接关闭默认开启的CSRF防御功能;另一种是配置需要的CSRF Token 如果是直接关闭开启的CSRF防御功能的话,直接重写SecurityConfig中的configure(HttpSecurity http)方法,这种方法台简单粗暴不推荐使用,如果强行关闭后网站可能会面临CSRF攻击的危险
//其他代码这里省略 http.csrf().disable();Security针对不同类型的数据修改请求提供了不同方式的CSRF Token配置看主要有针对Form表单数据修改的CSRF Token配置和针对Ajax数据修改的CSRF Token配置,下面就对这两种配置进行讲解
针对Form表单数据修改的CSRF Token配置针对Form表单数据修改请求,Security支持在Form表单中提供一个携带CSRF Token信息的隐藏域,与其他修改数据一起提交,这样后台就可以获取并验证该请求是否为安全
<div align="center"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> <form method="post" action="/updateUser"> 用户名: <input type="text" name="username" /><br /> 密 码: <input type="password" name="password" /><br /> <button type="submit">修改</button> </form> </div>th:name="${_csrf.parameterName}"获取Security默认提供的CSRF Token对应的key值_csrf,th:value="${_csrf.token}会获取Security默认随机生成的CSRF Token对应的value值,在Form表单中添加上述CSRF配置后,无需其他配置就可以正常实现数据修改请求,后台配置的Security会自动获取并识别请求中的CSRF Token信息并进行用户信息验证,从而判断是安全
需要说明的是,针对thymeleaf模板页面中的form表单数据修改请求,处理可以使用上述配置方式,还可以使用thymeleaf模板的in:action属性配置CSRF Token信息。该方法没有携带隐藏域,这是因为会默认携带CSRF Token信息,无需我们手动添加啦
<form method="post" th:action="@{/updateUser}"> 用户名: <input type="text" name="username" /><br /> 密 码: <input type="password" name="password" /><br /> <button type="submit">修改</button> </form> 针对Ajax数据修改的CSRF Token配置该方式中,Security提供了通过添加HTTP header头信息的方式携带CSRF Token信息
首先把页面<head>标签中添加<meta>,并配置CSRF Token信息
<head> <!-- 获取CSRF Token --> <meta name="_csrf" th:content="${_csrf.token}"/> <!-- 获取CSRF头,默认为X-CSRF-TOKEN --> <meta name="_csrf_header" th:content="${_csrf.headerName}"/> </head>上述代码中的两个<meta>分别用来设置CSRF Token信息的属性头和具体生成的Security Token值信息。其中在HTTP header头信息中携带CSRF请求头header参数的默认值为“X-CSRF-TOKEN”,而在请求头CSRF header对应的CSRF Token值是随机生成的
然后再具体的Ajax请求中获取<meta>中设置的CSRF Token信息并绑定再HTTP请求头中进行请求验证
$(function){ //获取<meta>设置的CSRF Token信息 var token = $("meta[name='_csrf']").attr(""content); var header = $("meta[name='_csrf_header']").attr(""content); //将头中的CSRF Token信息进行发送 $(document).ajaxSend(function(e,xhr,options){ xhr.setRequestHeader(header,token); }) }前面讲的都是通过Security对后台增加了权限控制,前端并没有做任何处理,前端页面显示还是对应的链接内容,用户体验较差。接下来我们在前面的基础上讨论如何使用Security域Thymleadf整合实现前端页面的管理
添加thymeleaf-extras-springsecurity5依赖 <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency>需要注意的是,我们添加的security5是由springboot统一整合管理的,如果我们要添加security4,需要手动加上<version>手动进行版本管理 2. 修改前端页面,使用Security相关标签进行页面控制 在项目首页index.html中引入security安全标签,并在页面中根据需要使用security标签进行显示控制
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>影视直播厅</title> </head> <body> <h1 align="center">欢迎进入电影网站首页</h1> <div sec:authorize="isAnonymous()"> <h2 align="center">游客您好,如果想查看电影<a th:href="@{/userLogin}">请登录</a></h2> </div> <div sec:authorize="isAuthenticated()"> <h2 align="center"><span sec:authentication="name" style="color: #007bff"></span>您好,您的用户权限为<span sec:authentication="principal.authorities" style="color:darkkhaki"></span>,您有权观看以下电影</h2> <form th:action="@{/mylogout}" method="post"> <input th:type="submit" th:value="注销" /> </form> </div> <hr> <div sec:authorize="hasRole('common')"> <h3>普通电影</h3> <ul> <li><a th:href="@{/detail/common/1}">飞驰人生</a></li> <li><a th:href="@{/detail/common/2}">夏洛特烦恼</a></li> </ul> </div> <div sec:authorize="hasAuthority('ROLE_vip')"> <h3>VIP专享</h3> <ul> <li><a th:href="@{/detail/vip/1}">速度与激情</a></li> <li><a th:href="@{/detail/vip/2}">猩球崛起</a></li> </ul> </div> </body> </html>页面顶部通过“xmlns:sec”引入了security安全标签,页面中根据需要编写了四个<div>模块
sec:authorize="isAnonymous()":判断用户是否未登录,只有匿名用户(未登录用户)才会显示“请登录”链接提示 同时,sec:authentication="name"和sec:authentication="principal.authorities"两个属性分别显示了登录用户名和权限sec:authorize="isAuthenticated()":判断用户是否已登录,只有认证用户(登录用户)才会显示登录用户信息和注销链接等提示sec:authorize="hasRole('common')":定义了只有角色为“common”(对应权限Authority为ROLE_common)且登录的用户才会显示普通电影列表信息sec:authorize="hasAuthority('ROLE_vip')":同上理 效果测试(略)该SpringBoot学习笔记学习自黑马程序员出版的《Spring Boot企业级开发教程》,是对知识点的整理和自我认识的梳理,如有不当之处,欢迎指出