第四阶段13-关于Spring Security框架续(认证和授权)
创始人
2024-06-02 21:45:34
0

关于防止伪造的跨域攻击

**伪造的跨域攻击:**此类攻击主要源自“服务器端对客户端的浏览器的信任”(本质上是浏览器对同一个服务器端会携带同一个Session ID),例如,用户在浏览器的第1个选项卡中登录了,在第2个、第3个等等其它选项卡访问同样的服务器,也会被视为“已登录”的状态。所以,假设某用户在浏览器的第1个选项卡中登录了网上银行,第2个选项卡打开其它某个恶意的网站(并不是网上银行),此网站中隐藏了一个网上银行发起请求的链接,并会自动提交(比较典型的做法就是把链接做为标签的src值,并隐藏此标签使之不显示),则会导致在第2个选项卡中打开恶意网站时就向网上银行发出了请求,网上银行接收到此请求时,也会视为“已登录”的状态!虽然通过这种手段基本上无法实现财产的窃取,但仍可以做一些其它的数据窃取。

**典型的防御手段:**在“非前后端分离”的开发模式下,服务器端在生成表单时,会在表单中隐藏一个具有“唯一性”或较强的“随机性”的值,正常提交表单时,此值会随着表单数据一并提交到服务器端,所以,服务器端会根据是否正确的提交了这个值,来判断是否是通过正常方式提交的请求。以Spring Security的默认登录表单为例:
请添加图片描述

**注意:**在“前后端分离”的项目中,由于服务器端不负责生成各表单页面,所以,也无法在表单中添加UUID值,则客户端提交的请求中也无法提交正确的UUID值,所以,这种防御机制并不适用!

在Spring Security的配置类中,调用HttpSecurity对象的csrf().disable()方法可以禁用此防御机制,则POST请求不再被要求提交UUID值,是可以正常使用的!例如:

@Override
protected void configure(HttpSecurity http) throws Exception {// 禁用“防止伪造的跨域攻击”这种防御机制http.csrf().disable();// 暂不关心其它配置的代码
}

关于BCrypt算法

BCrypt算法是目前用于处理密码加密存储时最安全的算法之一!

在Spring Security框架中,自带了BCryptPasswordEncoder类,用于执行加密与对比。

public class BCryptTests {BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();@Testvoid encode() {String rawPassword = "123456";System.out.println("原文:" + rawPassword);for (int i = 0; i < 10; i++) {String encodedPassword = passwordEncoder.encode(rawPassword);System.out.println("密文:" + encodedPassword);}// 原文:123456// 密文:$2a$10$i9E1pAtpQXIV4uUOjNr7ve/SWM9BDQl4hxq7Qz.kYDeLHTJwKhr7K// 密文:$2a$10$GCBOO7x7r8TjACK.ujA3oeft2uoKBTUm2L8UbWTRlnaugIupQB2Aq// 密文:$2a$10$osWYVZT3aBnG9h9ACiS8Mup3Hu4gjx1Lf46OIrjmseICX1cEPXZO6// 密文:$2a$10$r9l2Yced59FDWSFgh.mvmeAEyVQ9utfnEJJKuawO/17ndBT/xCF1q// 密文:$2a$10$A2DhHyIOTmWFbekRwM.wuOMSYQ1xVail7q3b1jkM4bdOjNJmfHuFK// 密文:$2a$10$YdvML3WsSlMghscfry/zkO67Yp8HZuVakMAXXDtuN.xmZUjVGcp5i// 密文:$2a$10$F.SxK1hRERQN6rwDIKw2BeD/wNXndICBs7pB9KJ7.rb9hqGVmpeeS// 密文:$2a$10$vtLCxMWTylqcqLwdcQpzIO00oT.lmULR9aQoelfP4BP6/0FBa2ckG// 密文:$2a$10$NX.BX7byguflK6G4/lQhVuJZZbekko.6h69Tilvdu2uMGHJuQCxi.// 密文:$2a$10$PNwmIXSRmy5yISsdbW1/Jusq4xwRx1KbkNGu.uJC2fYiHRQjOkWvS}@Testvoid matches() {String rawPassword = "123456";String encodedPassword = "$2a$10$osWYVZT3aBnG9h9ACiS8Mup3Hu4gjx1Lf46OIrjmseICX1cEPXZO6";boolean result = passwordEncoder.matches(rawPassword, encodedPassword);System.out.println("原文:" + rawPassword);System.out.println("密文:" + encodedPassword);System.out.println("匹配结果:" + result);}}

BCrypt算法是一个运算效率极低的算法,可以非常有效的避免穷举式的暴力破解,这也是BCrypt算法的核心优势之一!

关于PasswordEncoder

PasswordEncoder是一个接口,BCryptPasswordEncoderNoOpPasswordEncoder都是其典型的实现类。

Spring Security框架在处理认证时(判断尝试登录的账号的密码是否正确时),会自动使用PasswordEncoder中的matches()方法将原文和密文进行对比,所以:

  • 你需要配置某个PasswordEncoder对象到Spring容器中
  • UserDetailsService接口中的loadUserByUsername()方法,返回的UserDetails接口类型的对象中的password应该与你配置的PasswordEncoder使用的算法是对应的

使用前后端分离的登录

首先,要保证默认的登录表单不被启用,即:在Spring Security的配置类中,不再使用http.formLogin()方法!

然后:

  • 需要使用控制器(Controller)接收来自客户端提交的用户名和密码
    • 建议自定义POJO将客户端提交的数据封装起来
  • 在Service层中处理登录的认证
    • 具体的处理,仍交由Spring Security来实现,需要调用AuthenticationManager(认证管理器)对象的authenticate()方法,则Spring Security会自动调用UserDetailsService接口对象的loadUserByUsername()方法获取用户信息详情并自动验证此用户是否允许登录

所以,在项目的根包下创建pojo.dto.AdminLoginDTO类,用于封装客户端提交的用户名和密码:

@Data
public class AdminLoginDTO implements Serializable {private String username;private String password;
}

然后,在Spring Security的配置类(自定义的SecurityConfiguration类)中重写authenticationManagerBean()方法,并在此方法上添加@Bean注解,则Spring会自动调用此方法,得到AuthenticationManager类型的对象,并保存在Spring容器中,后续,需要AuthenticationManager时可以自动装配!

@Bean // 重要
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();
}

**注意:**以上重写的是authenticationManagerBean()方法,而不是authenticationManager()方法!如果重写错误,在后续某此测试中会出现死循环,导致内存溢出!

IAdminService接口中添加抽象方法:

void login(AdminLoginDTO adminLoginDTO);

AdminServiceImpl中重写以上方法:

@Autowired
private AuthenticationManager authenticationManager;@Override
public void login(AdminLoginDTO adminLoginDTO) {log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);Authentication authentication = new UsernamePasswordAuthenticationToken(adminLoginDTO.getUsername(), adminLoginDTO.getPassword());authenticationManager.authenticate(authentication);
}

AdminController中添加处理请求的方法:

// http://localhost:9081/admins/login
@PostMapping("/login")
public JsonResult login(AdminLoginDTO adminLoginDTO) {log.debug("开始处理【管理员登录】的请求,参数:{}", adminLoginDTO);adminService.login(adminLoginDTO);return JsonResult.ok();
}

另外,还需要将/admins/login添加到Spring Security配置类中的白名单中:

@Override
protected void configure(HttpSecurity http) throws Exception {// 白名单String[] urls = {"/doc.html","/**/*.js","/**/*.css","/swagger-resources","/v2/api-docs","/admins/login" // 新增,重要};// 暂不关心其它代码
}

完成后,重启项目,通过API文档测试访问:

  • 当用户名不存在时:

    org.springframework.security.authentication.InternalAuthenticationServiceException: UserDetailsService returned null, which is an interface contract violation
    
  • 当密码错误时:

    org.springframework.security.authentication.BadCredentialsException: 用户名或密码错误
    
  • 当账号被禁用时:

    org.springframework.security.authentication.DisabledException: 用户已失效
    

由于每种错误都会抛出某个异常,则应该先在ServiceCode中补充各种错误对应的业务状态码的枚举值:

public enum ServiceCode {OK(20000),ERR_BAD_REQUEST(40000),ERR_NOT_FOUND(40400),ERR_UNAUTHORIZED(40100), // 新增ERR_UNAUTHORIZED_DISABLED(40110), // 新增ERR_FORBIDDEN(40300), // 新增ERR_CONFLICT(40900),ERR_INSERT(50000),ERR_DELETE(50100),ERR_UPDATE(50200),ERR_SELECT(50300),ERR_UNKNOWN(99999);// 暂不关心其它代码}

然后在全局异常处理器中添加处理异常的方法:

@ExceptionHandler({InternalAuthenticationServiceException.class,BadCredentialsException.class})
public JsonResult handleAuthenticationException(AuthenticationException e) {log.warn("程序运行过程中出现AuthenticationException,将统一处理!");log.warn("异常信息:{}", e.getMessage());String message = "登录失败,用户名或密码错误!";return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED, message);
}@ExceptionHandler
public JsonResult handleDisabledException(DisabledException e) {log.warn("程序运行过程中出现DisabledException,将统一处理!");log.warn("异常信息:{}", e.getMessage());String message = "登录失败,此账号已经被禁用!";return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED_DISABLED, message);
}

关于通过认证的标准

在Spring Security中,为每个客户端都分配了一个SecurityContext,会根据SecurityContext中是否存在认证信息来判断是否已通过认证,即:

  • SecurityContext中存在认证信息:已通过认证
  • SecurityContext中没有认证信息:未通过认证

同时,SecurityContext默认是基于Session的,所以,也符合Session的某些特征,例如默认的有效期。

在项目中,可以通过SecurityContextHolder的静态方法getContext()方法,得到当前客户端对应的SecurityContext对象!

所以,在AdminServiceImpl中处理认证时,当通过认证,需要获取返回结果,并且,将返回结果存入到SecurityContext中:

@Override
public void login(AdminLoginDTO adminLoginDTO) {log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);// 执行认证Authentication authentication = new UsernamePasswordAuthenticationToken(adminLoginDTO.getUsername(), adminLoginDTO.getPassword());// 【调整】以下调用方法时需要获取返回值Authentication authenticationResult= authenticationManager.authenticate(authentication);// 【新增】将认证信息存入到SecurityContextSecurityContext securityContext = SecurityContextHolder.getContext();securityContext.setAuthentication(authenticationResult);
}

关于authenticate()的认证结果

当调用AuthenticationManagerauthenticate()方法执行认证,且认证通过时,此方法的返回结果例如:

2023-02-07 15:07:27.011 DEBUG 9212 --- [nio-9081-exec-1] c.t.c.p.service.impl.AdminServiceImpl    : 认证通过,结果:UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=root, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[暂时给个山寨权限,暂时没有作用,只是避免报错而已]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[暂时给个山寨权限,暂时没有作用,只是避免报错而已]
]

在认证结果中的Principal属性的值就是User类型的,本质上,就是自定义的UserDetailsServiceImplementationloadUserByUsername()方法的返回值!

识别当事人

在由框架调用的方法中,可以在参数列表中添加当事人类型的参数,并在此参数上添加@AuthenticationPrincipal注解,则框架会自动为此参数注入值,例如:

@GetMapping("")
//                                            ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 注解
//                                                                     ↓↓↓↓ 当事人的数据类型
public JsonResult> list(@AuthenticationPrincipal User user) {log.debug("开始处理【查询管理员列表】的请求,参数:无");log.debug("当事人:{}", user);List list = adminService.list();return JsonResult.ok(list);
}

其实,以上当事人的数据,就是UserDetailsServiceImpl中的loadUserByUsername()返回的UserDetails类型结果!

**注意:**当在处理请求的方法上添加了参数后,API文档会误以为这是需要由客户端提交的请求参数,实际此参数是由Spring Security框架来注入的,所以,应该在此参数上添加@ApiIgnore注解,使得API文档框架忽略此参数:

@GetMapping("")
//                                            ↓↓↓↓↓↓↓↓↓↓ 新增注解
public JsonResult> list(@ApiIgnore @AuthenticationPrincipal User user) {log.debug("开始处理【查询管理员列表】的请求,参数:无");log.debug("当事人:{}", user);List list = adminService.list();return JsonResult.ok(list);
}

由Spring Security提供的User类型实现了UserDetails接口,但是,User类型中的属性不一定足以满足编程需求,典型的问题就是:User类型中没有id属性,则后续在控制器中也无法得知登录的用户的ID。

可以自定义类,实现UserDetails接口,或继承自User类,在UserDetailsServiceImpl中的loadUserByUsername()方法中,返回自定义类的对象,则后续在控制器中可以注入此对象!

在项目的根包下创建security.AdminDetails类,继承自User类:

@ToString(callSuper = true)
public class AdminDetails extends User {@Getterprivate Long id;public AdminDetails(Long id, String username, String password, boolean enabled,Collection authorities) {super(username, password, enabled,true, true, true, authorities);this.id = id;}}

并且,在UserDetailsServiceImpl中的loadUserByUsername()方法中,返回AdminDetails的对象:

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {log.debug("Spring Security调用了loadUserByUsername()方法,参数:{}", s);AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);log.debug("从数据库查询用户名【{}】匹配的信息,结果:{}", s, loginInfo);if (loginInfo == null) {return null; // 暂时}// ========== 以下是调整后的代码 ==========List authorities = new ArrayList<>();SimpleGrantedAuthority authority = new SimpleGrantedAuthority("这是临时使用的一个山寨的权限");authorities.add(authority);AdminDetails adminDetails = new AdminDetails(loginInfo.getId(),loginInfo.getUsername(),loginInfo.getPassword(),loginInfo.getEnable() == 1,authorities);log.debug("即将向Spring Security返回UserDetails对象:{}", adminDetails);return adminDetails;
}

接下来,就可以在控制器中将当事人类型改为自定义的数据类型:

@GetMapping("")
//                                                                     ↓↓↓↓↓↓↓↓↓↓↓↓ 自定义类型的当事人
public JsonResult> list(@AuthenticationPrincipal AdminDetails adminDetails) {log.debug("开始处理【查询管理员列表】的请求,参数:无");log.debug("当事人:{}", adminDetails); // 可以获取IDlog.debug("当事人的ID:{}", adminDetails.getId());log.debug("当事人的用户名:{}", adminDetails.getUsername());List list = adminService.list();return JsonResult.ok(list);
}

授权访问

首先,需要调整原有的“根据用户名查询管理员的登录信息”功能,将用户名对应的管理员的权限列表查询出来,需要执行的SQL语句大致是:

SELECTams_admin.id,ams_admin.username,ams_admin.password,ams_admin.enable,ams_permission.value
FROM ams_admin
LEFT JOIN ams_admin_role ON ams_admin.id=ams_admin_role.admin_id
LEFT JOIN ams_role_permission ON ams_admin_role.role_id=ams_role_permission.role_id
LEFT JOIN ams_permission ON ams_role_permission.permission_id=ams_permission.id
WHERE ams_admin.username='root';

AdminLoginInfoVO中,添加属性,以表示管理员信息中的“权限列表”:

@Data
public class AdminLoginInfoVO implements Serializable {// 暂不关心原有的其它代码// ========== 以下是新增的属性 ==========/*** 权限列表*/private List permissions;}

并在AdminMapper.xml中调整:


ams_admin.id,ams_admin.username,ams_admin.password,ams_admin.enable,ams_permission.value







接下来,在UserDetailsServiceImplloadUserByUsername()方法,返回的数据中应该包含正确的权限:

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {log.debug("Spring Security调用了loadUserByUsername()方法,参数:{}", s);AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);log.debug("从数据库查询用户名【{}】匹配的信息,结果:{}", s, loginInfo);if (loginInfo == null) {return null; // 暂时}// ========== 重要 ===========List permissions = loginInfo.getPermissions();List authorities = new ArrayList<>();for (String permission : permissions) {SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);authorities.add(authority);}AdminDetails adminDetails = new AdminDetails(loginInfo.getId(),loginInfo.getUsername(),loginInfo.getPassword(),loginInfo.getEnable() == 1,authorities);log.debug("即将向Spring Security返回UserDetails对象:{}", adminDetails);return adminDetails;
}

至此,当每个管理员登录时,当前程序都会把对应的权限列表存入到SecurityContext中的认证信息中!

接下来,可以开启“基于方法的权限检查”,并且,在需要检查权限的方法上,通过注解配置权限!

则在Spring Security的配置类上,添加注解,以开启“基于方法的权限检查”:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 新增
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
}

然后,在AdminController中:

// http://localhost:9081/admins
@PreAuthorize("hasAuthority('/ams/admin/read')") // 配置权限
@GetMapping("")
public JsonResult> list(@AuthenticationPrincipal AdminDetails adminDetails) {// 暂不关心方法的实现
}

如果使用无此权限的账号提交请求,服务器端会出现错误:

org.springframework.security.access.AccessDeniedException: 不允许访问

则需要在ServiceCode中补充对应的业务状态码(此前应该已经事先添加了):ERR_FORBIDDEN(40300),然后,在GlobalExceptionHandler中补充处理以上异常的方法:

@ExceptionHandler
public JsonResult handleAccessDeniedException(AccessDeniedException e) {log.warn("程序运行过程中出现AccessDeniedException,将统一处理!");log.warn("异常信息:{}", e.getMessage());String message = "禁止访问,您当前登录的账号无此操作权限!";return JsonResult.fail(ServiceCode.ERR_FORBIDDEN, message);
}

相关内容

热门资讯

系统如何与安卓互通,技术融合与... 你有没有想过,你的手机系统竟然能和安卓系统这么默契地互通有无?这就像是一场跨越科技界的友谊赛,让我们...
安卓系统 扫码枪,安卓系统下扫... 你有没有想过,在繁忙的超市收银台,那些快速流畅的扫码操作,背后其实隐藏着一个小小的英雄——安卓系统扫...
平板插卡推荐安卓系统,安卓系统... 你有没有想过,你的平板电脑是不是也能像智能手机一样,随时随地扩充存储空间呢?没错,这就是今天我要跟你...
安卓系统固件安装失败,原因排查... 最近是不是你也遇到了安卓系统固件安装失败的问题?别急,让我来给你详细说说这个让人头疼的小麻烦,让你一...
ios系统和安卓区别,系统差异... 你有没有发现,现在手机市场上,iOS系统和安卓系统就像是一对双胞胎,长得差不多,但性格却截然不同。今...
安卓系统2.3优酷,优酷的崛起... 你有没有发现,安卓系统2.3时代的那股怀旧风?那时候,优酷可是视频界的巨头,多少人都是看着优酷长大的...
安卓导航系统密封,安卓导航系统... 你有没有发现,现在手机导航系统越来越智能了?尤其是安卓系统的导航,简直就像一个贴心的导航小助手,带你...
a版安卓11系统,a版深度解析... 你知道吗?最近手机界可是炸开了锅,各大品牌纷纷发布了搭载a版安卓11系统的手机。这可不是什么小打小闹...
安卓系统的模拟吉他,随时随地弹... 你有没有想过,在手机上也能弹奏吉他呢?没错,就是那种模拟吉他的安卓系统应用,让你随时随地都能享受音乐...
王者适配的安卓系统,深度解析适... 你有没有发现,最近玩《王者荣耀》的小伙伴们都在议论纷纷,说新出的安卓系统简直是为王者量身定做的!没错...
安卓系统自动定位关闭,隐私保护... 你有没有发现,手机里的安卓系统有时候会自动定位,这可真是让人又爱又恨啊!有时候,我们并不想让别人知道...
安卓系统电量耗尽测试,全面解析... 手机电量耗尽,这可是每个手机用户都头疼的问题。你有没有想过,你的安卓手机在电量耗尽前,到底经历了哪些...
如何升级车载安卓系统,车载安卓... 亲爱的车主朋友们,你是不是也和我一样,对车载安卓系统升级这件事充满了好奇和期待呢?想象当你驾驶着爱车...
安卓办公哪个系统好,深度解析哪... 你有没有想过,在安卓办公的世界里,哪个系统才是你的最佳拍档呢?在这个信息爆炸的时代,选择一个既强大又...
安卓系统差劲怎么解决,重拾流畅... 你有没有发现,安卓系统有时候真的让人头疼得要命?手机卡顿、应用崩溃、电池续航短,这些问题是不是让你抓...
喜欢安卓系统的原因,探索用户偏... 你有没有发现,身边的朋友、同事,甚至家人,越来越多的人开始使用安卓手机了呢?这可不是简单的潮流,而是...
安卓系统金立手机,品质生活新选... 你有没有发现,最近安卓系统下的金立手机突然火了起来?没错,就是那个曾经陪伴我们走过无数时光的金立手机...
无安卓系统的电视,新型无系统电... 亲爱的读者们,你是否厌倦了那些充斥着安卓系统的电视?想要尝试一些新鲜玩意儿?那就跟我一起探索一下无安...
麒麟系统能刷安卓系统吗,轻松刷... 你有没有想过,你的麒麟手机能不能装上安卓系统呢?这可是个让人好奇不已的问题。现在,就让我来带你一探究...
手机公司安卓系统吗,手机公司引... 你有没有想过,为什么你的手机里装的是安卓系统而不是苹果的iOS呢?这背后可是有着不少故事和门道的哦!...