Spring Security
Spring Security是主要解决认证(Authenticate)和授权(Authorization)的框架。
1、添加依赖
在Spring Boot项目中,添加spring-boot-starter-security
依赖项。
<!-- Spring Security:处理认证与授权 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
注意:以上依赖项是带有自动配置的,一旦添加此依赖,整个项目中所有的访问,默认都是必须先登录才可以访问的,在浏览器输入任何此服务的URL,都会自动跳转到默认的登录页面。默认的用户名是user
,默认的密码是启动项目时自动生成的随机密码,在服务器端的控制台可以看到此密码。当登录后,会自动跳转到此前尝试访问的页面。
Spring Security默认使用Session机制保存用户的登录状态,所以,重启服务后,登录状态会消失。在不重启的情况下,可以通过 /logout
访问“退出登录”页面,确定后也可以清除登录状态。
2、关于在Service中调用Security的认证机制:
当需要调用Security框架的认证机制时,需要使用AuthenticationManager
对象,可以在Security配置类中重写authenticationManager()
方法,在此方法上添加@Bean
注解,由于当前类本身是配置类,所以Spring框架会自动调用此方法,并将返回的结果保存到Spring容器中:
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
在IAdminService
中添加处理登录的抽象方法:
void login(AdminLoginDTO adminLoginDTO);
在AdminServiceImpl
中,可以自动装配AuthenticationManager
对象:
@Autowired
private AuthenticationManager authenticationManager;
并实现接口中的方法:
@Override
public void login(AdminLoginDTO adminLoginDTO) {
// 日志
log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);
// 调用AuthenticationManager执行认证
Authentication authentication = new UsernamePasswordAuthenticationToken(
adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
authenticationManager.authenticate(authentication);
log.debug("认证通过!");
}
3、在控制器中接收登录请求,并调用Service:
在根包下创建pojo.dto.AdminLoginDTO
类:
@Data
public class AdminLoginDTO implements Serializable {
private String username;
private String password;
}
在AdminController
中添加处理请求的方法:
@ApiOperation("管理员登录")
@ApiOperationSupport(order = 50)
@PostMapping("/login")
public JsonResult<Void> login(AdminLoginDTO adminLoginDTO) {
log.debug("准备处理【管理员登录】的请求:{}", adminLoginDTO);
adminService.login(adminLoginDTO);
return JsonResult.ok();
}
为了保证能对以上路径直接发起请求,需要将此路径(/admins/login
)添加到Security配置类的“白名单”中。
完成后,启动项目,可以通过Knife4j的调试来测试登录,当登录成功时将响应正确,当用户名或密码错误时,将响应错误(需要统一处理异常)。注意:即使登录成功,也不可以实现其它请求的访问!
4、处理登录成功的管理的权限列表
目前,存入到Security上下文中的认证信息(Authentication对象)并不包含有效的权限信息(目前是个假信息),为了后续能够判断用户的权限,需要:
- 当认证(登录)成功后,取出管理员的权限,并将其存入到JWT数据中
- 后续的请求中的JWT应该已经包含权限,则可以从JWT中解析出权限信息,并存入到认证信息(Authentication对象)中
- 在操作过程中,应该先将权限列表转换成JSON再存入到JWT中,在解析JWT时,得到的权限信息也是一个JSON数据,需要将其转换成对象才能继续使用
关于JSON格式的转换,有许多工具都可以实现,例如:fastjson
<!-- fastjson:实现对象与JSON的相互转换 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
在AdminServiceImpl
处理登录时,当认证成功时,需要从认证结果中取出权限列表,转换成JSON字符串,并存入到JWT中:
// 原有其它代码
Collection<GrantedAuthority> authorities = loginUser.getAuthorities();
log.debug("认证结果中的权限列表:{}", authorities);
String authorityListString = JSON.toJSONString(authorities); // 【重要】将权限列表转换成JSON格式,用于存储到JWT中
// 生成JWT时的Claims相关代码
claims.put("authorities", authorityListString);
log.debug("生成JWT,向JWT中存入authorities:{}", authorityListString);
5、处理异常
在登录时,可能出现:
- 用户名错误:
BadCredentialsException
- 密码错误:
BadCredentialsException
- 账号被禁用:
DisabledException
在访问时,可能出现: - 无此权限:
AccessDeniedException
以上异常都可以由统一处理异常的机制进行处理,则先在ServiceCode
中添加对应的业务状态码:
/**
* 未授权的访问
*/
Integer ERR_UNAUTHORIZED = 40100;
/**
* 未授权的访问:账号禁用
*/
Integer ERR_UNAUTHORIZED_DISABLED = 40101;
/**
* 禁止访问,通常是已登录,但无权限
*/
Integer ERR_FORBIDDEN = 40300;
然后,在统一处理异常的类中,添加对相关异常的处理:
@ExceptionHandler
public JsonResult<Void> handleBadCredentialsException(BadCredentialsException e) {
String message = "登录失败,用户名或密码错误!";
log.debug("处理BadCredentialsException:{}", message);
return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED, message);
}
@ExceptionHandler
public JsonResult<Void> handleDisabledException(DisabledException e) {
String message = "登录失败,此账号已禁用!";
log.debug("处理DisabledException:{}", message);
return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED_DISABLED, message);
}
@ExceptionHandler
public JsonResult<Void> handleAccessDeniedException(AccessDeniedException e) {
String message = "访问失败,当前登录的账号无此权限!";
log.debug("处理AccessDeniedException:{}", message);
return JsonResult.fail(ServiceCode.ERR_FORBIDDEN, message);
}