Spring Security
基于表达式的控制访问
access() 方法的使用
之前所学的用户权限判定: hasRole 、 hasAnyRole、 hasAnyAuthority 、permitAll 等实际上底层源码都是调用access(表达式)
如hasRole :
public ExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistry hasRole(String role) {
return this.access(ExpressionUrlAuthorizationConfigurer.hasRole(role));
}
permitAll:
public ExpressionUrlAuthorizationConfigurer<H>.ExpressionInterceptUrlRegistry permitAll() {
return this.access("permitAll");
}
所以:可以通过access()实现与其权限控制相同的功能
access()结合自定义方法实现权限控制
第一步:定义一个接口
public interface MyAccessService {
//定义权限判断方法
boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
第二步:实现类
@Service
public class MyAccessServiceImpl implements MyAccessService {
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
Object principal = authentication.getPrincipal();
if(principal instanceof UserDetails){
UserDetails userDetails=(UserDetails)principal;
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
//判断用户的所有权限中是否包含所要访问的该URL,没有返回false,有的话返回true
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}
}
第三步:配置
.antMatchers("/main1.html").access("@myAccessServiceImpl.hasPermission(request,authentication)")
//注意,hasPermission方法中的参数必须与实现类MyAccessServiceImpl中的一样
当我们访问/main1.html时,显示无权访问,因为当前用户的权限中没有加入该URL:**”/main1.html”**
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登录成功</h1>
<h1><a href="/main1.html">前往main1页面</a></h1>
</body>
</html>
{"status":"error","msg":"权限不足,请联系管理员"}
在自定义登录逻辑中给该用户的权限中加上”/main1.html”,便可成功访问。
如下:
.......
return new User(username,password,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,/main1.html"));
.......
基于注解的访问控制
在Spring Security中提供了一些访问控制的注解。这些注解都是默认不可用的,需要通过**@EnableGlobalMethodSecurity**进行开启后使用。
如果设置的条件允许,程序正常执行,如果不允许会报500
org.springframework.security.access.AccessDeniedException: 不允许访问
这些注解可以写到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("ROLE_aaa")
@RequestMapping("/toMain")
public String toMain(){
return "redirect:main.html";
}
//如果当前登录用户没有角色aaa,则无法跳转到main.html,且会报500
@PreAuthorize/@PostAuthorize
@PreAuthorize/@PostAuthorize都是方法和类级别的注解。
- @PreAuthorize:表示访问方法或类在执行之前先判断权限,大多数情况下都是使用这个注解,注解的参数和access()方法参数取值相同,都是权限表达式。
- @PostAuthorize:表示方法或类执行结束后判断权限,此注解很少使用到。
开启注解
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class SpringsecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecurityDemoApplication.class, args);
}
}
使用注解
//PreAuthorize的表达式允许以 ROLE_ 开头,也可以不以ROLE_开头,直接写角色名,如下:
@PreAuthorize("hasRole('admin')")
@RequestMapping("/toMain")
public String toMain(){
return "redirect:main.html";
}
RememberMe功能
Spring Security中Remember Me为”记住我”功能,用户只需在登录时添加remember-me复选框,取值为true。Spring Security会自动把用户信息存储在数据源中,以后就可以不登录进行访问。
Demo
添加依赖
导入mybatis启动器用时导入MySQL驱动
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
配置文件:
spring:
#mybatis配置
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/security?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
登录页面:
<!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"><br/>
密码:<input type="password" name="password"><br/>
<!--记住我 的name必须为remember-me,如果想自定义的话必须在配置类中指定-->
记住我:<input type="checkbox" name="remember-me" value="true"><br/>
<input type="submit" value="登录">
</form>
</body>
</html>
配置类:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
//重写configure方法来自定义登录页面
@Override
protected void configure(HttpSecurity http) throws Exception {
//表单提交
http.formLogin()
.loginProcessingUrl("/login")
.loginPage("/login.html")
.successForwardUrl("/toMain")
.failureHandler(new MyAuthenticationFailureHandler("error.html"));
//授权认证
http.authorizeRequests()
.antMatchers("/error.html").permitAll()
.antMatchers("/login.html").permitAll()
.antMatchers("/main1.html").access("@myAccessServiceImpl.hasPermission(request,authentication)")
.anyRequest().authenticated();
//关闭csrf防护
http.csrf().disable();
//403处理
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
//remember me
//---------------------------------------remember me-------------------------------------
http.rememberMe()
//自定义登录逻辑
.userDetailsService(userDetailsService)
//持久层对象
.tokenRepository(persistentTokenRepository)
//失效时间,默认是两周,单位秒
.tokenValiditySeconds(60);
//---------------------------------------end-------------------------------------
}
@Bean
public PasswordEncoder getPw(){
//返回一个PasswordEncoder实例
return new BCryptPasswordEncoder();
}
//------------------------------persistentTokenRepository-------------------------------
@Bean
public PersistentTokenRepository getPersistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository= new JdbcTokenRepositoryImpl();
//设置数据源
jdbcTokenRepository.setDataSource(dataSource);
//第一次启动时自动建表,用户存储"记住我"的数据,第二次启动时一定要注释掉,不注释会报错
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
//---------------------------------------end-------------------------------------
}
项目启动会生成一个表:
mysql> use security
Database changed
mysql> show tables
-> ;
+--------------------+
| Tables_in_security |
+--------------------+
| persistent_logins |
+--------------------+
1 row in set (0.00 sec)
mysql> desc persistent_logins;
+-----------+-------------+------+-----+-------------------+-----------------------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+-------------+------+-----+-------------------+-----------------------------+
| username | varchar(64) | NO | | NULL | |
| series | varchar(64) | NO | PRI | NULL | |
| token | varchar(64) | NO | | NULL | |
| last_used | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+-----------+-------------+------+-----+-------------------+-----------------------------+
4 rows in set (0.01 sec)
在登录时,选定记住我,会将用户的登录信息保存在这个表中,在有效时间内,不许登录就能访问。
mysql> select * from persistent_logins;
+----------+--------------------------+--------------------------+---------------------+
| username | series | token | last_used |
+----------+--------------------------+--------------------------+---------------------+
| admin | MwHzropZq3D8GKK4Vz3wgg== | jqLKe2uduDhJY0wPJ+JQeA== | 2021-05-09 12:00:19 |
+----------+--------------------------+--------------------------+---------------------+
1 row in set (0.00 sec)
在Thymeleaf中SpringSecurity的使用
Spring Security可以在一些视图技术中进行控制显示效果。例如JSP
和Thymeleaf
,在非前后端分离的且使用SpringBoot的项目中多使用Thymeleaf
作为视图展示技术。
Thymeleaf
对SpringSecurity的支持都放在thymeleaf-extras-springsecurityX中,目前最新版为5,。
添加依赖:
<!--thymeleaf整合SpringSecurity-->
<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/extras/spring-security">
获取属性
在UsernamePasswordAuthenticationToken中保存着当前用户的所有信息
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 540L;
private final Object principal;
private Object credentials;
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
.........get方法和构造略
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
private final Collection<GrantedAuthority> authorities;
private Object details;
private boolean authenticated = false;
.........get方法和构造略
可以在HTML中通过使用sec:authentication=""
获取UsernamePasswordAuthenticationToken中所有的getXXX的内容,包括父类AbstractAuthenticationToken中的getXXX的内容。
根据源码得出下列属性:
- name:登录账户名
- principal:登录主体,在自定义登录逻辑中是UserDetails
- credentials:凭证
- authorities:权限和角色
- details:实际上是
WebAuthenticationDetails
实例,可以获取remoteAddress(客户端IP)和sessionID(当前SessionID)
WebAuthenticationDetails:
public class WebAuthenticationDetails implements Serializable {
private static final long serialVersionUID = 540L;
private final String remoteAddress;
private final String sessionId;
.....略get和构造方法
demo页面:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>demo</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:
@RequestMapping("/demo")
public String demo(){
return "demo";
}
登录后访问:http://localhost:8080/demo
结果:
登录账号:admin
登录账号:admin
凭证:
权限和角色:[/main1.html, admin, normal]
客户端地址:0:0:0:0:0:0:0:1
sessionid:C7C0776F3F35DE05B13BB53E79BE1E48
权限判断
SpringSecurity在thymeleaf中的权限判断和Shiro差不多:
通过权限判断
<button sec:authorize="hasAuthority('insert')">新增</button>
<button sec:authorize="hasAuthority('admin')">修改</button>
通过角色判断
<button sec:authorize="hasRole('delete')">删除</button>
<button sec:authorize="hasRole('admin')">查询</button>
退出登录
一个超链接即可:
<a href="/logout">退出登录</a>
但是退出后URL中会携带参数logout:
http://localhost:8080/login.html?logout
如果不想显示这个参数的话,可以配置类中指定退出要跳转的页面:
//退出登录
http.logout()
.logoutSuccessUrl("/login.html");
SpringSecurity中的CSRF
//关闭csrf防护
http.csrf().disable();
如果没有这行代码会导致用户无法被认证。
什么是CSRF
CSRF(Cross-site request forgery)跨站请求伪造,也被称为”OneClick Attack”或者”Session Riding”,通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议,IP地址,端口中任何一个不相同就是跨域请求。
客户端与服务进行交互时,由于http 协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id 可能被第三方恶意劫持,通过这个session id 向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。
SpringSecurity中的CSRF
从Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为token(token 在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。
如果不关闭CSRF,则在请求登录时,要携带服务端生成的CSRF的token:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<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/>
<!--记住我 的name必须为remember-me,如果想自定义的话必须在配置类中指定-->
记住我:<input type="checkbox" name="remember-me" value="true"><br/>
<input type="submit" value="登录">
</form>
</body>
</html>
- Post link: http://sovzn.github.io/2021/05/07/SpringSecurity-02/
- Copyright Notice: All articles in this blog are licensed under unless otherwise stated.
若没有本文 Issue,您可以使用 Comment 模版新建。
GitHub Issues