SpringSecurity
1. antMatchers()
-
方法定义:
public C antMatchers(String... antPatterns)
方法参数不定,每个参数都是一个ant表达式,用于匹配URL规则。
规则如下:
?: 匹配一个字符
*: 匹配0个活多个字符
**: 匹配0个活多个目录在实际项目中我们经常要放行所有静态资源,如放行js文件夹下的所有脚本。
.antMatchers("/js/**").perimitAll()
还有一种配置方式是只要是.js文件都放行
.antMatchers("/**/*.js").perimitAll()
- 方法定义
public C antMathchers(HttpMethod method, String... antPatterns)
指定请求方式,只有匹配ant表达式,并且用指定请求方式时候,请求才会被放行。否则,请求将被拦截。
2. regexMatchers()
通过正则表达式匹配。
3. mvcMatchers()
通过在项目的配置文件中增加配置:
spring.mvm.servlet.path=/API
可以为所有请求的Controller增加统一前缀/API,访问所有Controller的时候,需要带上/API前缀,才能访问到。如果我们要在项目中放行特定的资源,不被拦截,那么可以这样写:
.mvcMatchers("/demo").servletPath("/API").perimitAll()
放行了 /API/demo这个请求。
4. Spring Security访问控制
从源码 ExpressionUrlAuthorizationConfigurer 中可以看到
static final String permitAll = "permitAll"; // 放行所有请求
private static final String denyAll = "denyAll"; // 拒绝所有请求
private static final String anonymous = "anonymous"; // 匿名访问,与permitAll差不多,但是请求会走特定的拦截器
private static final String authenticated = "authenticated"; // 需要认证
private static final String fullyAuthenticated = "fullyAuthenticated"; // 需要完全认证
private static final String rememberMe = "rememberMe"; // 记住我,下次访问可以直接访问
Spring Security 有6中访问控制。
5. hasAuthority 和 hasAnyAuthority
判断用户是否具有特定权限
6. hasRole 和 hasAnyRole
判断用户是否具有特定角色。
7. hasIpAddress
通过指定的Ip地址,进行访问控制。
8. 自定义403页面
当操作被禁止后,会返回到403页面,而Spring Security提供的403页面不够友好,此时,我们可以自定义403页面。
-
实现AccessDeniedHandler接口
@Componente 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 writer = response.getWriter(); writer.write("{\"status\":\"error\", \"msg\": \"has no authority, please concact us\"}"); writer.flush(); writer.close(); } }
-
修改configure方法
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .usernameParameter("username123") .passwordParameter("password123") .loginProcessingUrl("/login") .loginPage("/login.html") //.successForwardUrl("/toMain") //.successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com")) .successHandler(new MyAuthenticationSuccessHandler("/main.html")) // 登录成功后,到达的页面 //.failureForwardUrl("/toError"); .failureHandler(new MyAuthenticationFailureHandler("/error.html")); http.authorizeRequests() .antMatchers("/error.html").permitAll() .antMatchers("/login.html").permitAll() .antMatchers("/main1.html").hasAuthority("abc") .anyRequest().authenticated(); http.csrf().disable(); http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler); // 当操作被禁止后,执行的handler } @Bean public PasswordEncoder getPwd() { return new BCryptPasswordEncoder(); } }
main.html页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> main.html <a href="/main1.html">jump to main1.html</a> </body> </html>
到达main.html页面后,点击jump to main1.html连接,跳转会出现:
{“status”:“error”, “msg”: “has no authorization”}
此时,说明我们自定义的跳转逻辑生效了。.antMatchers("/main1.html").hasAuthority(“abc”) 判断用户是否有abc权限,如果没有,就会执行代码 http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler); 就会执行我们自定义的myAccessDeniedHandler的逻辑。
9. 基于表达式的访问控制
通过看 permitAll 和 hasAuthority 方法的源码:
public ExpressionInterceptUrlRegistry permitAll() {
return access(permitAll);
}
public ExpressionInterceptUrlRegistry hasAuthority(String authority) {
return access(ExpressionUrlAuthorizationConfigurer.hasAuthority(authority));
}
注意到,最终都是调用access方法。因此,我们也可以改写我们前面的config方法,实现同样的访问控制。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.usernameParameter("username123")
.passwordParameter("password123")
.loginProcessingUrl("/login")
.loginPage("/login.html")
//.successForwardUrl("/toMain")
//.successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
.successHandler(new MyAuthenticationSuccessHandler("/main.html"))
//.failureForwardUrl("/toError");
.failureHandler(new MyAuthenticationFailureHandler("/error.html"));
http.authorizeRequests()
//.antMatchers("/error.html").permitAll()
.antMatchers("/error.html").access("permitAll()") // 基于表达式的访问控制
//.antMatchers("/login.html").permitAll()
.antMatchers("/login.html").access("permitAll()")
// .antMatchers("/main1.html").hasAuthority("abc")
.antMatchers("/main1.html").access("hasAuthority('abc')")
.anyRequest().authenticated();
http.csrf().disable();
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
}
Spring Security 支持的表达式有:
- hashRole(role)
- hasAnyRole([role1,role2,…])
- hasAuthority([authority])
- hasAnyAuthority([authority, authority2,…])
- principal
- authentication
- permitAll
- denyAll
- isAnonymous()
- isRememberMe()
- isAuthenticated()
- isFullyAuthenticated()
- hasPermission(Object target, Object permission)
10. 基于注解的访问控制
在Spring Security中提供了一些访问控制的注解。这些注解默认都是不可用的,需要通过@EnableGlobalMethodSecurity进行开启后使用,该注解标注在启动类或 继承了WebSecurityConfigurerAdapter 的配置类上。
- @EnableGlobalMethodSecurity(securedEnabled = true)
会激活@Secured,开启基于角色注解的访问控制,将注解@Secured(“ROLE_角色”) 标注在Controller的方法上,那么只有有相应角色的用户才能够访问该方法。 - @EnableGlobalMethodSecurity(prePostEnabled = true)
会激活@PreAuthorize和@PostAuthorize,这两个注解都是方法或类级别的注解。- @PreAuthorize表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和access()方法参数值相同,都是权限表达式。
如@PreAuthorize(“hasRole(‘ROLE_abc’)”) 表示拥有abc角色可以访问,但是也可以在写成@PreAuthorize(“hasRole(‘abc’)”) 但是如果是在 configure方法中使用access表达式配置,角色前面不能以ROLE_开头 - @PostAuthorize 表示方法或类执行结束后判断权限,此注解很少被使用。
- @PreAuthorize表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和access()方法参数值相同,都是权限表达式。
11. RememberMe功能实现
Spring Security 中Remember Me 为“记住我”功能,用户只需要在登录时添加remember-me复选框,取值为true。Spring Security会自动把用户信息存储到数据源中,以后就可以不登录进行访问了。
- 添加依赖
Spring Security实现Remember Me功能时底层实现依赖Spring-JDBC,所以需要导入Spring-JDBC。以后多使用MyBatis框架而很少直接导入Spring-jdbc,所以此处导入mybatis启动器同时还需要添加MySQL驱动。<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.19</version> </dependency>
- 配置application.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://ip:port/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=password
- 在mysql中创建数据库并指定编码
create database security CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
- 在configure方法中增加rememberMe的逻辑
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyAccessDeniedHandler myAccessDeniedHandler; @Autowired private UserDetailsService userDetailsService; @Autowired private DataSource dataSource; @Autowired private PersistentTokenRepository persistentTokenRepository; @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .usernameParameter("username123") .passwordParameter("password123") .loginProcessingUrl("/login") .loginPage("/login.html") //.successForwardUrl("/toMain") //.successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com")) .successHandler(new MyAuthenticationSuccessHandler("/main.html")) //.failureForwardUrl("/toError"); .failureHandler(new MyAuthenticationFailureHandler("/error.html")); http.authorizeRequests() //.antMatchers("/error.html").permitAll() .antMatchers("/error.html").access("permitAll()") //.antMatchers("/login.html").permitAll() .antMatchers("/login.html").access("permitAll()") // .antMatchers("/main1.html").hasAuthority("abc") .antMatchers("/main1.html").access("hasAuthority('abc')") .anyRequest().authenticated(); http.csrf().disable(); http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler); http.rememberMe() // 自定义登录逻辑 .userDetailsService(userDetailsService) // 持久层对象 .tokenRepository(persistentTokenRepository); } @Bean public PasswordEncoder getPwd() { return new BCryptPasswordEncoder(); } @Bean public PersistentTokenRepository getPersistentTokenRepository() { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); // 这样代码只需要在第一次启动时,存在。其他时候需要注释掉,否则会报错 jdbcTokenRepository.setCreateTableOnStartup(true); return jdbcTokenRepository; } }
- login.html 页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form action="/login" method="post"> Username:<input type="text" name="username123"/><br/> Password: <input type="password" name="password123"/><br/> Remember Me: <input type="checkbox" name="remember-me"> <input type="submit" value="submit"> </form> </body> </html>
- 当我们启动成功后,发现数据库中已经自动创建了表:persistent_logins。当我们登录login.html页面,勾选Remember Me复选框,并且输入正确的用户名密码后,我们登录到了main.html页面。此时可以看到数据库中已经有了用户的数据。
当我们关闭浏览器,重新打开后,输入http://localhost:8080/main.html,此时,直接就会跳转到main.html页面,没有进行登录拦截。也就是“记住我”功能实现。 - 默认情况下,Token的失效时间时两周,我们可以自定义token实现时间。
只需要将原来的代码改为:http.rememberMe() .tokenValiditySeconds(60) // token 60s后失效,默认是两周时间 //.rememberMeParameter() // 这里是自定义表单上的 记住我的参数,默认是remember-me。 .userDetailsService(userDetailsService) .tokenRepository(persistentTokenRepository);
12. Spring Security在thymeleaf中的使用
thymleaf 是spring官方推荐展示层,通常用在前后端不分离的项目中。Spring Security 在 thymeleaf中的使用,
- 引入thymeleaf 和 springsecurity 相关依赖
<dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
- 在html页面中引入thymeleaf命名空间和 security命名空间
<!DOCTYPE html> <!-- 要使用thymeleaf和springsecurity,相应的命名空间必须添加--> <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 charset="UTF-8"> <title>Title</title> </head> <body> <span sec:authentication="name">123</span> <span sec:authentication="principal.username">456</span> <span sec:authentication="credentials">456</span> <span sec:authentication="authorities">456</span> <span sec:authentication="details.remoteAddress">456</span> <span sec:authentication="details.sessionId">456</span> </body> </html>
- 使用thymeleaf,需要controller配合,实现页面跳转。
@RequestMapping("demo") public String toDemo() { return "demo"; // 条状到demo.html页面 }
- 上面在thymeleaf页面中,就可以获取到SpringSecurity用户相关的数据,而且,我们在thymeleaf页面,还可以进行一些权限的判断。
修改自定义的UserDetailsServiceImpl类
在demo.html页面增加如下代码:@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("exec loadUserByUsername"); if (!"admin".equals(username)){ throw new UsernameNotFoundException("username is not found"); } String password = passwordEncoder.encode("123"); return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList( "admin,normal,/insert,ROLE_abc" // 增加了 /insert权限 和 abc的角色,角色之前需要有ROLE_前缀 )); } }
<br> 通过权限判断: <button sec:authorize="hasAuthority('/insert')">add</button> 通过角色判断: <button sec:authorize="hasRole('abc')">add</button>
13. 退出登录功能
我们需要在 configure中添加如下代码,即可实现登录后的退出功能。
-
在main.html页面中增加
<a href="/logout">exit</a>
但是这样登出后我们会回到login.html页面,但是浏览器地址栏会显示http://localhost:8080/login.html?logout。如果我们不想要?logout这一串,我们需要在configure方法中配置。
-
configure方法
http.logout() .logoutUrl("/logout") // 自定义登出的url,默认是/logout,可以自己定义 .logoutSuccessUrl("/login.html"); // 登出成功后返回的页面
通过查看登出的源码,我们大概能够了解到,SpringSecurity帮我们做了两件事:让HttpSession实现和清除认证状态。
14. CSRF
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack”或者Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
客户端与服务器进行交互时,由于http协议本身的无状态性,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户身份。在跨域的情况下,session id可能被第三方恶意劫持,通过这个session id向服务器发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。
Spring Security中的CSRF
从Spring Security4开始CSRF防护默认开启。默认情况下会拦截所有请求,进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csfr值为token的内容。如果客户端携带的token与服务器端的token匹配成功,则正常访问。
在我们前面的代码中,默认是增加了一行代码:
http.csrf().disable();
关闭了跨请求伪造功能。
开启跨站请求伪造功能,将该行代码注释掉。并且我们之前在 protected void configure(HttpSecurity http) 方法中,配置了直接跳转到登录页面:.loginPage("/login.html")。现在我们修改成通过controller跳转:
现在的 protected void configure(HttpSecurity http) 代码:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.usernameParameter("username123")
.passwordParameter("password123")
.loginProcessingUrl("/login")
.loginPage("/showLogin") // 跳转到controller而不是原来的login.html
//.successForwardUrl("/toMain")
//.successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
.successHandler(new MyAuthenticationSuccessHandler("/main.html"))
//.failureForwardUrl("/toError");
.failureHandler(new MyAuthenticationFailureHandler("/error.html"));
http.authorizeRequests()
//.antMatchers("/error.html").permitAll()
.antMatchers("/error.html").access("permitAll()")
//.antMatchers("/login.html").permitAll()
.antMatchers("/showLogin").access("permitAll()") // 放行/showLogin请求
// .antMatchers("/main1.html").hasAuthority("abc")
.antMatchers("/main1.html").access("hasAuthority('abc')")
.anyRequest().authenticated();
//http.csrf().disable();
http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
http.rememberMe()
.tokenValiditySeconds(60)
//.rememberMeParameter()
.userDetailsService(userDetailsService)
.tokenRepository(persistentTokenRepository);
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html");
}
增加controller 的showLogin方法
@RequestMapping("showLogin")
public String showLogin() {
return "login";
}
在引入thmeleaf的情况下,这个controller返回的是视图的名字,spring会找template目录下的login.html视图。
这个视图修改如下:
<!DOCTYPE html>
<!-- 引入了thmeleaf命名空间-->
<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 charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
<!-- 开启csrf情况下,我们登录的时候,需要携带参数名_csrf值为token的一个参数,否则,即使用户名密码正确,也不能够登录 -->
<input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}">
Username:<input type="text" name="username123"/><br/>
Password: <input type="password" name="password123"/><br/>
Remember Me: <input type="checkbox" name="remember-me">
<input type="submit" value="submit">
</form>
</body>
</html>