前言
本篇文章将会将认证以及授权一并学习完成,话不多说,那就让我们开始SpringSecurity的第二段旅程吧。
认证的进阶(四)
自定义登录成功处理器
之前我们登录成功以后会去Controller实现一个跳转,由于目前的项目都实现了前后端分离,所以后端就不需要再管页面跳转了,交给前端即可。假如我们登录成功后需要跳转到csdn界面,如果将网址直接写在successForwardUrl,就会出现问题的。
.successForwardUrl("https://www.youkuaiyun.com")
如果这样写的话,你会发现输入正确的用户名和密码,却显示登录失败。
既然提供的无法使用,那我们就自己来自定义实现。
我们点进successForwardUrl方法看一下它是如何实现的。
public FormLoginConfigurer<H> successForwardUrl(String forwardUrl) {
this.successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl));
return this;
}
可以看出登录成功的跳转逻辑是由ForwardAuthenticationSuccessHandler类来实现的,点进去看一下。
public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
//跳转地址
private final String forwardUrl;
//构造方法
public ForwardAuthenticationSuccessHandler(String forwardUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> {
return "'" + forwardUrl + "' is not a valid forward URL";
});
this.forwardUrl = forwardUrl;
}
//实现跳转的逻辑
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//熟悉的请求转发
request.getRequestDispatcher(this.forwardUrl).forward(request, response);
}
}
到此处呢,思路就清晰了,我们可以自定义实现AuthenticationSuccessHandler接口,之后将其装入successHandler即可。
具体实现
1、实现AuthenticationSuccessHandler
public class SuccessHandlerImpl implements AuthenticationSuccessHandler {
private String url;
public SuccessHandlerImpl(String url) {
this.url = url;
}
/**
* @param httpServletRequest
* @param httpServletResponse
* @param authentication 登录成功后的用户信息
* @return void
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
httpServletResponse.sendRedirect(url);
}
}
2、装入处理器中
//.successForwardUrl("https://www.youkuaiyun.com")
.successHandler(new SuccessHandlerImpl("http://www.youkuaiyun.com"))
需要注意的是,这俩个方法不能同时出现的。
3、测试(登录成功,跳转到了csdn主页)。
自定义登录失败处理器
既然可以自定义登录成功的处理器,那么登录失败的处理器自然也是有的。其实它的处理逻辑和成功是一模一样的,这里就不多叙述了,直接实现。
具体实现
1、实现AuthenticationFailureHandler
public class FailureHandlerImpl implements AuthenticationFailureHandler {
private String url;
public FailureHandlerImpl(String url) {
this.url = url;
}
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletRequest.getRequestDispatcher(url).forward(httpServletRequest,httpServletResponse);
}
}
2、装入处理器中
//.failureForwardUrl("/toError")
.failureHandler(new FailureHandlerImpl("/toError"))
3、测试(登陆失败后跳转error.html)
权限的进阶
现在我们基本上实现了用户登录时的认证,那么不同用户的权限我们该如何实现呢,在之前对于SpringSecurity进行配置的时候,其实是已经进行了部分的权限设置的。
http.authorizeRequests()
//若为login.html则不需要进行验证
.antMatchers("/login.html").permitAll()
.antMatchers("/error.html").permitAll()
//所有请求必须认证(上面进行设置过的除外)
.anyRequest().authenticated();
我们设置了所有的请求都必须进行认证,但是对/login.html和/error.html进行了一个放行。
接下来,我们先看一下这些方法所代表的具体意义。
匹配URL的方法
方法名 方法作用 antMatchers() 配置一个requestMather的string数组,参数为ant路径格式,直接匹配url anyRequest() 匹配任意url,无参 ,最好放在最后面 authorizeRequests() URL权限配置 本质上是可以将任意数量的antMachers、anyRequest连接起来,以满足web安应用安全规则的需要。这些规则按照给定的顺序发挥作用,必须将最具体(细粒度)的请求路径放在前面,而最不具体的路径(anyRequest)放在最后面。否则后面的路径配置将会被前面的覆盖。
保护URL的方法
方法名 方法作用 permitAll() 指定URL无需保护(无条件允许访问),一般用于静态资源文件 authenticated() 保护URL,允许认证过的用户访问 hasRole(String role) 如果用户具备给定角色的话,就允许访问 hasAuthority(String auth) 如果用户具备给定权限的话,就允许访问 hasAnyRole(String…) 如果用户具备给定角色中的某一个的话,就允许访问 hasAnyAuthority(String…) 如果用户具备给定权限中的某一个的话,就允许访问 hasIpAddress(String) 如果请求来自给定IP地址的话,就允许访问 rememberMe() 如果用户是通过Remember-me功能认证的,就允许访问 fullyAuthenticated() 如果用户是完整认证的话(不是通过Remember-me功能认证的)就允许访问 denyAll() 无条件拒绝所有访问 anonymous() 允许匿名用户访问 access(String) 如果给定的SpEL表达式计算结果为true,就允许访问
Tip:Ant风格的路径表达式
通配符
通配符 说明 ? 匹配任意单字符 * 匹配0或者任意数量的字符 ** 匹配0或更多的目录 最长匹配原则:选择越详细越长的匹配路径进行匹配
匹配URL方法
权限的控制一般是通过筛选指定的url,然后对筛选的url指定访问控制方法,从而实现权限的控制。首先看一下一般用来匹配url的方法。
antMatchers
可以配置一个Ant风格的路径表达式,同正则表达式一样进行范围匹配URL。
现在让我们来,简单的试一下。
1、在/static/images下添加1.jpg和2.jpg俩张图片
2、配置SecurityConfig进行配置
//放行images下的所有图片
.antMatchers("/images/*").permitAll()
//只放行1.jpg一个图片
.antMatchers("/images/1.jpg").permitAll()
3、分别进行测试访问(http://localhost:8080/images/1.jpg、http://localhost:8080/images/2.jpg)
regexMatchers
该筛选方法是基于正则表达式来实现url匹配的。
同样对上面的俩张图片进行筛选,我们将前面的antMatchers注释掉,在SecurityConfig中添加以下代码
//匹配所有jpg格式的图片
.regexMatchers(".+[.]jpg").permitAll()
访问控制方法
基本控制
当我们筛选到指定的url之后,就需要去控制该url访问的权限了,比如上面的permitAll()方法就是不做任何验证直接通过。
我们进入permitAll()方法
public ExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistry permitAll() {
return this.access("permitAll");
}
它会调用一个access方法,参数为访问控制的权限。我们看一下其它的几种
//指定URL无需保护(无条件允许访问)
static final String permitAll = "permitAll";
//无条件拒绝所有访问
private static final String denyAll = "denyAll";
//允许匿名用户访问
private static final String anonymous = "anonymous";
//保护URL,允许认证过的用户访问
private static final String authenticated = "authenticated";
//如果用户是完整认证的话(不是通过Remember-me功能认证的)就允许访问
private static final String fullyAuthenticated = "fullyAuthenticated";
//如果用户是通过Remember-me功能认证的,就允许访问
private static final String rememberMe = "rememberMe";
分别有与之对应的方法,我们可以进行不同的访问控制。
权限控制
我们在实际业务中,用户一般都是会分不同的等级,比如普通用户和vip用户等,不同的用户所能访问的内容也是不同的,所以接下来让我们来实现基于权限的控制。
正常的逻辑是我们从数据库的权限表中查到对应的用户所拥有的权限。然后装填到User中。
//在UserDetailsServiceImmpl直接返回一个vip权限
return new User(s,password, AuthorityUtils.commaSeparatedStringToAuthorityList("vip"));
1、增加一个vip页面,只有vip用户才可以去访问
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>vip界面</title>
</head>
<body>
欢迎回来,尊贵的vip用户!!!!
</body>
</html>
2、在SecurityConfig中设置权限的控制
.antMatchers("/vip.html").hasAuthority("vip")
Tip1:如果需要设置多个权限,可以使用hasAnyAuthority(String …)方法。
Tip2:需要说明的是权限的控制对大小写是敏感的,确保大小写要一致
3、测试
首先测试未通过。
.antMatchers("/vip.html").hasAuthority("vip1")
登录后点击跳转,页面如下
Tip:403,禁止访问,常用于权限的控制
.antMatchers("/vip.html").hasAuthority("vip")
登录后点击跳转,页面如下
角色控制
像经典的RBAC模型,都是基于角色的访问控制的,我们将相对应的权限呢赋予相应的角色,从而直接控制角色来进行一个访问,这样不仅可以简单易懂,并且有助于解耦,我们只需要将对应的权限给与对应的角色即可,不需要在上面的基于权限的代码中进行修改了。接下来实现一下。
1、首先要做的是,如何将角色添加到UserDetails中
return new User(s,password, AuthorityUtils.commaSeparatedStringToAuthorityList("vip,ROLE_vip"));
它和权限的添加位置是一样的,并且是需要在角色前加一个ROLE_的,所以说我们从数据库里面查出对应的角色时,如果没有加ROLE 则我们需要处理一下。
2、配置SecurityConfig
.antMatchers("/vip.html").hasRole("vip")
Tip1:如果需要设置多个权限,可以使用hasAnyRole(String …)方法。
Tip2:需要说明的是角色的控制对大小写是敏感的,确保大小写要一致
Tip3:添加时必须去掉ROLE_,否则会报错的
IP控制
在实际发布的项目中,有时候我们可能只允许某个ip进行访问,这是我们该如何实现呢。
1、配置SecurityConfig
.antMatchers("/vip.html").hasIpAddress("127.0.0.1")
2、测试
http://127.0.0.1:8080/vip.html
可以通过
http://localhost:8080/vip.html
不可以通过
基于表达式得访问控制
上面permitAll()方法内部,调用得其实就是access方法。基于表达式就是使用access方法,方法得参数为**SpEL表达式,用来说明具体功能。
SpEL表达式
authentication | 用户的认证对象 |
---|---|
denyAll | 结果始终为false |
hasAnyRole(list of roles) | 如果用户被授予了列表中的人已制定角色,结果为true |
hasRole(role) | 如果用户被授予了指定的角色,结果为true |
hasIpAddress(IP) | 如果来自指定的IP,结果为true |
isAnonymous() | 如果当前用户为匿名用户,结果为true |
isAuthenticated() | 如果当前用户进行了认证,结果为true |
isFullyuAuthenticated() | 如果当前用户进行了完整认证,结果为true |
isRememberMe() | 如果当前用户是通过Remember-me自动认证的,结果为true |
permitAll | 结果为true |
principal | 用户的principal对象 |
可以使用and、or等逻辑运算符,如access(“hasRole(‘ROLE_SPITTER’) and hasIpAddress(‘192.168.1.2’)”)。
拒绝访问处理
在前面我们没有权限访问的时候,会报一个403禁止访问,实际业务中,我们不可能直接将这个页面展示给用户的,接下来让我们来处理一下。
跳转到指定页面
1、设置页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
权限不足,<a href="main.html">点击跳回</a>主页面
</body>
</html>
2、设置SecurityConfig
http.exceptionHandling().accessDeniedPage("/error_auth.html");
设置拒绝处理器
1、创建DenyHandler类
@Component
public class DenyHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpServletResponse.setContentType("application/json; charset=UTF-8");
Writer writer = httpServletResponse.getWriter();
writer.write("{\"code\":\"403\",\"msg\":\"权限不足\"}");
writer.flush();
writer.close();
}
}
2、设置
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
3、测试
RememberMe功能的实现
在我们平时登录的时候,我们是可以看见有一个选项为记住密码,当我们在指定时间内,在次访问该网站就不需要再进行一个登录操作了,这个功能就是RememberMe的功能。
具体实现
因为该功能的实现是将相关数据存储在了数据库,下次的登录时会使用,所以需要添加相关数据源的依赖。
1、添加依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
2、配置数据源
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: *****
url: jdbc:mysql://localhost:3306/db_security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
3、配置SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//配置数据源
@Autowired
private DataSource dataSource;
//持久层对象
@Autowired
private PersistentTokenRepository persistentTokenRepository;
//自定义登录逻辑对象
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.rememberMe()
//设置持续时间,单位为秒
.tokenValiditySeconds(60)
//设置前端的参数的名
.rememberMeParameter("rememberMe")
//设置登录逻辑
.userDetailsService(userDetailsService)
//设置持久层对象
.tokenRepository(persistentTokenRepository);
//将csrf关掉,之后会说
http.csrf().disable();
}
//持久层逻辑对象
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
//设置数据源
jdbcTokenRepository.setDataSource(dataSource);
//自动创建表,只在第一次启动时使用,之后将其注释掉即可
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
4、前端页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用户:<input type="text" name="username_update">
密码:<input type="password" name="password_update">
记住我:<input type="checkbox" name="rememberMe" value="true"/>
<input type="submit" value="提交">
</form>
</body>
</html>
5、测试
退出登录
1、前端设置
<!--必须为/logout,可以在配置类里面进行一个配置-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录成功,<a href="vip.html">点击跳转</a>到vip界面,<a href="/logout">登出</a>
</body>
</html>
2、配置类
http.logout()
//登出后跳转的页面
.logoutSuccessUrl("/login.html")
//设置登出的路径
.logoutUrl("/logout");
也可以设置logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler),与之前的处理器相同,可以做一些登出的处理。
CSRF
我们在上面一直是有执行如下的代码的
http.csrf().disable();
那么为何要关掉它呢,以及这个CSRF到底是个什么东西呢?
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。
通俗的来说,攻击者会伪造为你的身份,以你的名义进行一些恶意的请求,比如以你的名义给别人发邮件、发消息、或者给别人转账等,从而造成一些个人隐私的泄露以及财产的安全问题。
CSRF的原理
需要说明的是,http协议是一个无状态协议,所以从浏览器发送来的请求,我们的服务器是并不能进行识别是由谁发送而来的,所以在用户登录之后,会发送一个SessionId到浏览器端,并将其存储在cookie中,当我们之后发起请求时,服务器会去本地通过cookie中的SessionId去验证当前用户是否授权登录。所以说服务器识别的只有SessionId,但是这个id是由谁发送的服务器就不能够在进行一个识别了。如上网站B利用你的SessionId去登录时,网站A的服务器会认为是你本人在操作的,这也是为什么称为跨站请求伪造的缘故了。
解决方法
对于该种攻击的防御的方法是有很多种的,但是它的核心解决方法就是在客户端的页面增加一个随机数
比如在SpringSecurity种使用的就是在请求地址中添加一个token并进行验证。
1、对登录表单进行一个改造
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用户:<input type="text" name="username_update">
密码:<input type="password" name="password_update">
记住我:<input type="checkbox" name="rememberMe" value="true"/>
<!--固定参数-->
<input type="hidden" name="_csrf" th:value="${_csrf.token}" th:if="${_csrf}">
<input type="submit" value="提交">
</form>
</body>
</html>
记得添加Thymeleaf依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
2、将CSRF功能开启(将一下代码注释即可,默认是开启状态的)
//http.csrf().disable();
3、测试,登录
//表单数据
username_update: jiayi
password_update: 123456
_csrf: d4ed193a-3f5b-44a9-9eaf-ad2fb2cbc4dc //需要验证的token
总结
总的来说,如果可以把之前和现在的文章的代码理解以后,SpringSecurity的基本的认证和授权已经可以掌握了,当然一个框架的所有细节不可能全部讲到的,如果想更加深入的理解和学习这个框架,可以去Spring官方网站去查看的它的官方文档,上面会讲的更加的详细的。
下篇文章,将会去学习一下,我们登录时一直使用的第三方登录,也就是Oauth2。最后希望我们可以一起进步,争取早日成为大佬。