一、访问控制(授权)
1. 基于资源访问控制
-
查询用户的权限。
-
访问资源时判断用户是否具有指定的权限。
1.1 修改UserServiceImpl
@Service
public class UserServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
/**
* @param username 前端登录时提交的用户名
* @return 用户的认证信息,要求必须为UserDetails接口的实现类
* @throws UsernameNotFoundException 用户名没有找到时的异常
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username);
User myUser = userMapper.selectOne(wrapper);
if (myUser == null){
throw new UsernameNotFoundException("用户名不存在");
}
//查询用户拥有的权限
List<Menu> menus = menuMapper.selectByUserId(myUser.getId());
ArrayList<GrantedAuthority> list = new ArrayList<>();
menus.forEach(menu -> {
//获取的权限为null时,将null添加到SimpleGrantedAuthority会出现
//A granted authority textual representation is required异常信息
String permission = menu.getPermission();
if (permission != null && permission!=""){
list.add(new SimpleGrantedAuthority(permission));
}
});
//参数一:用户名 参数二:密码 参数三:权限
/*
* org.springframework.security.core.userdetails.User为UserDetails接口的实现类。
* 也可以自定义的User实现UserDetail接口提供对应的属性。
* */
org.springframework.security.core.userdetails.User user = new org.springframework.security.core.userdetails.User(
myUser.getUsername(), myUser.getPassword(), list);
return user;
}
}
1.2 修改配置类
//请求授权设置。
/*
* antMatchers():对指定的请求url进行控制
* permitAll():允许访问
* anyRequest():任意一个请求url的控制
* authenticated():认证后便可以访问
* hasAuthority():存在指定的权限可以访问
* hasAnyRole(String ...):存在指定的任意一个权限可以访问
* */
http.authorizeRequests()
.antMatchers("/login.html").permitAll() //login.html不需要被认证
.antMatchers("/stu/select").hasAuthority("stu:select") //需要有指定的权限
.antMatchers("/stu/insert").hasAuthority("stu:insert") //需要有指定的权限
.antMatchers("/stu/update").hasAuthority("stu:update") //需要有指定的权限
.antMatchers("/stu/del").hasAuthority("stu:del") //需要有指定的权限
.anyRequest().authenticated(); //其它任意的请求都必须被认证,必须认证(登录成功)后就可以直接访问
2. 基于角色访问控制
2.1 修改UserServiceImpl
官网要求:角色前必须添加 ROLE_ 前缀,角色才会生效。例如:ROLE_admin。
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
@Autowired
private RoleMapper roleMapper;
/**
* @param username 前端登录时提交的用户名
* @return 用户的认证信息,要求必须为UserDetails接口的实现类
* @throws UsernameNotFoundException 用户名没有找到时的异常
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username);
User myUser = userMapper.selectOne(wrapper);
if (myUser == null) {
throw new UsernameNotFoundException("用户名不存在");
}
/*
* org.springframework.security.core.userdetails.User为UserDetails接口的实现类。
* 也可以自定义的User实现UserDetail接口提供对应的属性。
* */
//查询用户拥有的权限
List<Menu> menus = menuMapper.selectByUserId(myUser.getId());
ArrayList<GrantedAuthority> list = new ArrayList<>();
menus.forEach(menu -> {
//获取的权限为null时,将null添加到SimpleGrantedAuthority会出现
//A granted authority textual representation is required异常信息
String permission = menu.getPermission();
if (permission != null && permission != "") {
list.add(new SimpleGrantedAuthority(permission));
}
});
//查询用户所有角色
List<Role> roles = roleMapper.selectByUserId(myUser.getId());
roles.forEach(role -> {
list.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
});
//参数一:用户名 参数二:密码 参数三:权限
org.springframework.security.core.userdetails.User user = new org.springframework.security.core.userdetails.User(
myUser.getUsername(), myUser.getPassword(), list);
return user;
}
}
2.2 修改配置类
//请求授权设置。
/*
* antMatchers():对指定的请求url进行控制
* permitAll():允许访问
* anyRequest():任意一个请求url的控制
* authenticated():认证后便可以访问
* hasAuthority():存在指定的权限可以访问
* hasAnyRole(String ...):存在指定的任意一个权限可以访问
* hasRole():存在指定的角色可以访问
* hasAnyRole():存在指定的任意一个角色可以访问
* */
http.authorizeRequests()
.antMatchers("/login.html").permitAll() //login.html不需要被认证
.antMatchers("/stu/select").hasAuthority("stu:select") //需要有指定的权限
.antMatchers("/stu/insert").hasAuthority("stu:insert") //需要有指定的权限
.antMatchers("/stu/update").hasRole("管理员") //需要有指定的角色
.antMatchers("/stu/del").hasRole("管理员") //需要有指定的角色
.anyRequest().authenticated(); //其它任意的请求都必须被认证,必须认证(登录成功)后就可以直接访问
二、基于注解的访问控制
在Spring Security中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过@EnableGlobalMethodSecurity进行开启后使用。
这些注解可以写到Service接口或方法上,也可以写到Controller或Controller的方法上。通常情况下都是写在控制器方法上的,控制接口URL是否允许被访问。
1. 注解介绍
1.1 @PreAuthorize
-
@PreAuthorize:表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解。
-
注意:必须在启动类@EnableGlobalMethodSecurity中设置prePostEnabled = true
1.2 @PostAuthorize
-
@PostAuthorize:表示方法或类执行结束后判断权限,此注解很少被使用到。
2 . 修改配置类
-
配置类中不再需要配置基于资源和基于角色的权限控制。
3. 修改启动器
-
在启动类中通过@EnableGlobalMethodSecurity开启@PreAuthorize注解。
-
@EnableGlobalMethodSecurity(prePostEnabled = true)
4. 修改控制器
-
在控制器方法上添加@PreAuthorize。
@RestController
@RequestMapping("stu")
public class StudentController {
@RequestMapping("insert")
@PreAuthorize("hasAuthority('stu:select')")
public String insert(){
return "添加学生";
}
@RequestMapping("update")
@PreAuthorize("hasAuthority('stu:update')")
public String update(){
return "修改学生";
}
@RequestMapping("del")
@PreAuthorize("hasRole('ROLE_管理员')")
public String del(){
return "删除学生";
}
@RequestMapping("select")
@PreAuthorize("hasRole('ROLE_管理员')")
public String select(){
return "查询学生";
}
}
三、自定义403处理方案
在Spring Security 的 自定义配置类( WebSecurityConfigurerAdapter )中使用HttpSecurity 提供的 exceptionHandling() 方法用来处理异常。该方法构造出 ExceptionHandlingConfigurer 异常处理配置类。该配置类提供了两个实用接口:
-
AuthenticationEntryPoint 该类用来统一处理 AuthenticationException 异常
-
AccessDeniedHandler 该类用来统一处理 AccessDeniedException 异常
1. 新建类
新建类实现AccessDeniedHandler。
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
out.flush();
}
}
2. 修改配置类
配置类中重点添加异常处理器。设置访问受限后交给哪个对象进行处理。
@Autowired
private AccessDeniedHandler accessDeniedHandler;
//异常处理
http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler);
四、Spring Security整合Thymeleaf使用
在项目中添加此jar包的依赖和thymeleaf的依赖。
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
html添加命名空间。
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
1. 获取属性
根据源码得出下面属性:
name:登录账号名称
principal:登录主体,在自定义登录逻辑中是UserDetails
credentials:凭证
authorities:权限和角色
details:实际上是WebAuthenticationDetails的实例。可以获取remoteAddress(客户端ip)和sessionId(当前sessionId)
<!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 charset="UTF-8">
<title>Title</title>
</head>
<body>
登录账号:<span sec:authentication="name">123</span><br/>
登录账号:<span sec:authentication="principal.username">456</span><br/>
凭证:<span sec:authentication="credentials">456</span><br/>
权限和角色:<span sec:authentication="authorities">456</span><br/>
客户端地址:<span sec:authentication="details.remoteAddress">456</span><br/>
sessionId:<span sec:authentication="details.sessionId">456</span><br/>
</body>
</html>
2. 权限判断
如果用户具有指定的权限,则显示对应的内容;如果表达式不成立,则不显示对应的元素。
通过权限判断:
<button sec:authorize="hasAuthority('stu:insert')">新增</button>
<button sec:authorize="hasAuthority('stu:delete')">删除</button>
<button sec:authorize="hasAuthority('stu:update')">修改</button>
<button sec:authorize="hasAuthority('stu:select')">查看</button>
<br/>
通过角色判断:
<button sec:authorize="hasRole('管理员')">新增</button>
<button sec:authorize="hasRole('管理员')">删除</button>
<button sec:authorize="hasRole('管理员')">修改</button>
<button sec:authorize="hasRole('管理员')">查看</button>
五、Spring Security中CSRF
1. CSRF
CSRF英文全称叫做: cross-site request forgery,翻译过来叫做跨站请求伪造。spring security默认情况下是开启了csrf保护的。
CSRF 是致击者通过一些技术手段欺骗用户的浏览器,去访问一个用户曾经认证过的网站并执行恶意请求,例如发送邮件、发消息、甚至财产操作(如转账和购买商品)。由于客户端(浏览器)已经在该网站中认证过了,所以该网站会认为是真正用户在操作而执行请求(实际上并非用户的本意)。
2. Spring Security中CSRF
从Spring Security4开始CSRF防护默认开启。默认会拦截请求,以防止CSRF攻击应用程序处理。默认情况下会启用 CSRF 保护,,SpringSecurity CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。要求访问时携带参数名为_csrf值为token(token在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。
注意,这里面不包括GET、HEAD、TRACE、OPTIONS请求,这些请求还是会存在这种问题的。
3. Spring Security中CSRF原理
-
当服务器加载登录页面,先生成csrf对象,并放入作用域中,key为_csrf。
-
用户在提交登录表单时,会携带csrf的token。如果客户端的token和服务器的token匹配说明是自己的客户端,否则无法继续执行。
-
用户退出的时候,必须发起POST请求,且和登录时一样,携带csrf的令牌。
4. 实现
4.1 login.html
在项目resources下新建templates文件夹,并在文件夹中新建login.html页面。form表单中的第一行是必须存在的否则无法正常登录。
<!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/>
<input type="submit" value="登录"/>
</form>
</body>
4.2 修改配置类
在配置类中注释掉CSRF防护失效。