csmall-passport(Day14)

news/2025/2/2 2:03:36/

1. 使用JWT保存权限

UserDetailsServiceImpl中,调用的adminMapper.getLoginInfoByUsername()中已经包含用户的权限,则,在返回的UserDetails对象中封装权限信息:

UserDetails userDetails = User.builder().username(loginAdmin.getUsername()).password(loginAdmin.getPassword()).accountExpired(false).accountLocked(false).credentialsExpired(false).disabled(loginAdmin.getEnable() == 0).authorities(loginAdmin.getPermissions().toArray(new String[] {})) // 调整.build();

AdminServiceImpl中,执行认证且成功后,返回的Authentication对象中的“当事人”就是以上返回的UserDetails对象,所以,此对象中是包含了以上封装的权限信息的,则可以将权限信息取出并封装到JWT中。

需要注意:如果直接将权限(Collection<? extends GrantedAuthority>)存入到JWT数据中,相当于把Collection<? extends GrantedAuthority>转换成String,此过程会丢失数据的原始类型,且不符合自动反序列化格式,后续解析时,无法直接还原成Collection<? extends GrantedAuthority>类型!为解决此问题,可以先将Collection<? extends GrantedAuthority>转换成JSON格式的字符串再存入到JWT中,后续,解析JWT时得到的也会是JSON格式的字符串,可以反序列化为Collection<? extends GrantedAuthority>格式!

则先添加JSON工具类的依赖项:

<!-- fastjson:实现对象与JSON的相互转换 -->
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.75</version>
</dependency>

然后,在AdminServiceImpl中,先从认证成功的返回结果中取出权限,然后存入到JWT中:

// 从认证返回结果中取出当事人信息
User principal = (User) authenticateResult.getPrincipal();
String username = principal.getUsername();
log.debug("认证信息中的用户名:{}", username);
// ===== 以下是新增 ======
Collection<GrantedAuthority> authorities = principal.getAuthorities();
log.debug("认证信息中的权限:{}", authorities);
String authorityListString = JSON.toJSONString(authorities);
log.debug("认证信息中的权限转换为JSON字符串:{}", authorityListString);// 生成JWT,并返回
// 准备Claims值
Map<String, Object> claims = new HashMap<>();
claims.put("username", username);
claims.put("authorities", authorityListString); // 新增

最后,在JwtAuthorizationFilter中,解析JWT时,取出权限的JSON字符串,将其反序列化为符合Collection<? extends GrantedAuthority>的格式:List<SimpleGrantedAuthority>,并用于存入到认证信息中:

String username = claims.get("username", String.class);
log.debug("从JWT中解析得到【username】的值:{}", username);
String authorityListString = claims.get("authorities", String.class); // 新增
log.debug("从JWT中解析得到【authorities】的值:{}", authorityListString); // 新增// 准备权限,将封装到认证信息中
List<SimpleGrantedAuthority> authorityList= JSON.parseArray(authorityListString, SimpleGrantedAuthority.class);// 准备存入到SecurityContext的认证信息
Authentication authentication= new UsernamePasswordAuthenticationToken(username, null, authorityList);

2. 使用Spring Security控制访问权限

首先,需要在Spring Security的配置类上开启方法前的权限检查:

@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 新增
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {// 省略配置类中原有代码
}

然后,在需要对权限进行检查(控制)的控制器类的方法上,使用注解来配置权限,例如:

// http://localhost:9081/admins
@ApiOperation("查询管理员列表")
@ApiOperationSupport(order = 400)
@PreAuthorize("hasAuthority('/ams/admin/read')") // 新增
@GetMapping("")
public JsonResult<List<AdminListItemVO>> list() {log.debug("开始处理【查询管理员列表】的请求……");List<AdminListItemVO> list = adminService.list();return JsonResult.ok(list);
}

以上新增的@PreAuthorize("hasAuthority('/ams/admin/read')")就表示已经通过认证的用户必须具有 '/ams/admin/read' 权限才可以访问此请求路径(http://localhost:9081/admins),如果没有权限,将抛出org.springframework.security.access.AccessDeniedException: 不允许访问

由于无操作权限时会出现新的异常,则在GlobalExceptionHandler中补充对此类异常的处理:

@ExceptionHandler
public JsonResult<Void> handleAccessDeniedException(AccessDeniedException e) {log.debug("处理AccessDeniedException");Integer serviceCode = ServiceCode.ERR_FORBIDDEN.getValue();String message = "请求失败,当前账号无此操作权限!";return JsonResult.fail(serviceCode, message);
}

3. 在控制器中识别当前登录的用户

当已经通过认证的用户访问服务器时,将携带JWT数据,而JWT数据在过滤器(JwtAuthorizationFilter)就已经解析完成,如果在控制器中需要识别用户的身份,只在在过滤器将用户信息存储到认证信息(Authentication)中,并且,在控制器中获取相关数据!

通常,识别用户的身份时,需要获取当前登录的用户的id,Spring Security处理认证时,需要的用户信息的数据类型是UserDetails接口类型的,并且,Spring Security提供了User作为此接口类型的实现,但是,User类型中并没有id、头像、昵称等各软件设计时的个性化数据属性,在开发实践时,为了保证能够得到这些个性化数据,应该使用自定义类型实现UserDetails接口,或者,自定义类型继承自User类,并在UserDetailsServiceImpl中返回此类对象!

则在根包下创建security.AdminDetails类:

package cn.tedu.csmall.passport.security;import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;import java.util.Collection;public class AdminDetails extends User {private Long id;public AdminDetails(String username, String password, boolean enabled,Collection<? extends GrantedAuthority> authorities) {super(username, password, enabled, true, true, true, authorities);}public Long getId() {return id;}public void setId(Long id) {this.id = id;}}

在实现UserDetailsService接口时,此前返回的对象都是User对象,现在就可以返回自定义的AdminDetails对象了:

@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {log.debug("Spring Security自动调用loadUserByUsername()方法获取用户名为【{}】的用户详情……", s);AdminLoginInfoVO loginAdmin = adminMapper.getLoginInfoByUsername(s);log.debug("从数据库中查询到的用户信息:{}", loginAdmin);if (loginAdmin == null) {String message = "登录失败,用户名不存在!";log.warn(message);throw new BadCredentialsException(message);}List<String> permissions = loginAdmin.getPermissions();List<GrantedAuthority> authorities = new ArrayList<>();for (String permission : permissions) {authorities.add(new SimpleGrantedAuthority(permission));}AdminDetails adminDetails = new AdminDetails(loginAdmin.getUsername(), loginAdmin.getPassword(),loginAdmin.getEnable() == 1, authorities);adminDetails.setId(loginAdmin.getId());//        UserDetails userDetails = User.builder()
//                .username(loginAdmin.getUsername())
//                .password(loginAdmin.getPassword())
//                .accountExpired(false) // 账号是否已过期
//                .accountLocked(false) // 账号是否已锁定
//                .credentialsExpired(false) // 凭证是否已过期
//                .disabled(loginAdmin.getEnable() == 0) // 账号是否已禁用
//                .authorities(loginAdmin.getPermissions().toArray(new String[] {})) // 权限,【注意】必须调用此方法表示此用户具有哪些权限
//                .build();log.debug("即将向Spring Security框架返回UserDetails对象:{}", adminDetails);return adminDetails;
}

以上方法返回的对象仍是Spring Security处理认证时判断是否允许登录的对象,也是认证成功后返回的认证信息中的当事人,所以,在AdminServiceImpllogin()方法中,当认证成功后,可以获取认证信息中的当事人,并从中获取到id等信息,用于保存到JWT数据:

// 从认证返回结果中取出当事人信息
AdminDetails principal = (AdminDetails) authenticateResult.getPrincipal(); // 修改
Long id = principal.getId(); // 新增
log.debug("认证信息中的用户id:{}", id); // 新增
String username = principal.getUsername();
log.debug("认证信息中的用户名:{}", username);
Collection<GrantedAuthority> authorities = principal.getAuthorities();
log.debug("认证信息中的权限:{}", authorities);
String authorityListString = JSON.toJSONString(authorities);
log.debug("认证信息中的权限转换为JSON字符串:{}", authorityListString);// 生成JWT,并返回
// 准备Claims值
Map<String, Object> claims = new HashMap<>();
claims.put("id", id); // 新增
claims.put("username", username);
claims.put("authorities", authorityListString);

至此,当用户通过认证时,得到的JWT数据中将包含此用户的id。

通常,在控制器中需要识别用户的身份时,需要的信息可能有多个,例如用户的id、用户名等,可以将这些信息封装到自定义对象中,例如,在根包下创建security.LoginPrincipal类:

package cn.tedu.csmall.passport.security;import lombok.Data;import java.io.Serializable;@Data
public class LoginPrincipal implements Serializable {private Long id;private String username;}

然后,在过滤器(JwtAuthorizationFilter)中,当解析JWT时,就可以从中获取id与用户名,并使用这2个值来创建LoginPrincipal对象,最后,将LoginPrincipal对象封装到认证信息的当事人中:

Long id = claims.get("id", Long.class); // 新增
log.debug("从JWT中解析得到【id】的值:{}", id); // 新增
String username = claims.get("username", String.class);
log.debug("从JWT中解析得到【username】的值:{}", username);
String authorityListString = claims.get("authorities", String.class);
log.debug("从JWT中解析得到【authorities】的值:{}", authorityListString);// 准备权限,将封装到认证信息中
List<SimpleGrantedAuthority> authorityList= JSON.parseArray(authorityListString, SimpleGrantedAuthority.class);// 创建自定义的当事人类型的对象
LoginPrincipal loginPrincipal = new LoginPrincipal(); // 新增
loginPrincipal.setId(id); // 新增
loginPrincipal.setUsername(username); // 新增// 准备存入到SecurityContext的认证信息
Authentication authentication= new UsernamePasswordAuthenticationToken(loginPrincipal, null, authorityList);  // 修改了第1个参数值,改为loginPrincipal

至此,当客户端携带(最新的)JWT到服务器端时,过滤器可以解析得到idusername,并且,这些属性最终将保存到SecurityContext的认证信息中,则后续控制器可以随时获取这些信息,例如:

@ApiOperation("查询管理员列表")
@ApiOperationSupport(order = 400)
@PreAuthorize("hasAuthority('/ams/admin/read')")
@GetMapping("")
public JsonResult<List<AdminListItemVO>> list(// 下一行的参数声明是新增的@ApiIgnore @AuthenticationPrincipal LoginPrincipal loginPrincipal) {log.debug("开始处理【查询管理员列表】的请求……");log.debug("从SecurityContext中获取到的信息:"); // 新增log.debug("当事人id = {}", loginPrincipal.getId()); // 新增log.debug("当事人用户名 = {}", loginPrincipal.getUsername()); // 新增List<AdminListItemVO> list = adminService.list();return JsonResult.ok(list);
}

4. 关于secretKey值

AdminServiceImpl中生成JWT、在JwtAuthorizationFilter中解析JWT,都需要使用到相同的secretKey值,目前,在这2个代码片段中各自使用局部变量声明了此变量,并且,2个文件中的这2个变量的值是相同的,但是,各声明一个局部变量是不合理的!

可以在application.properties中添加自定义配置:

# 当前项目的自定义配置:JWT使用的secretKey
csmall.jwt.secret-key=97iuFDVDfv97iuk534Tht3KJR89kBGFSBgfds

然后,在这2个类中都添加:

@Value("${csmall.jwt.secret-key}")
private String secretKey;

各这2个类都可以读取到application.properties中的配置值,不必再各自声明secretKey局部变量了!

另外,建议将“JWT的有效时长”也进行类似的处理,例如:

# 当前项目的自定义配置:JWT的有效时长,以分钟为单位
csmall.jwt.duration-in-minute=10000

5. 处理解析JWT时可能出现的异常

由于解析JWT是在过滤器(JwtAuthorizationFilter)中执行的,而过滤器是Java EE中最早接收到请求的组件,如果此时出现异常,Spring MVC框架的相关组件还没有开始执行,即“全局异常处理器”是不会发挥作用的!

对于解析JWT可能出现的异常,应该由过滤器组件直接进行处理!

首先,在ServiceCode中补充新的业务状态码:

public enum ServiceCode {OK(20000),ERR_BAD_REQUEST(40000),ERR_UNAUTHORIZED(40100),ERR_UNAUTHORIZED_DISABLED(40110),ERR_FORBIDDEN(40300),ERR_NOT_FOUND(40400),ERR_CONFLICT(40900),ERR_INSERT(50000),ERR_DELETE(50100),ERR_UPDATE(50200),ERR_JWT_EXPIRED(60000), // 新增ERR_JWT_PARSE(60100); // 新增// 省略其它原有代码

然后,在过滤器中,解析JWT时,使用try...catch语法捕获并处理异常:

// 程序执行到此处,表示客户端携带了有效的JWT,则尝试解析
log.debug("获取到的JWT被视为【有效】,则尝试解析……");
Claims claims = null;response.setContentType("application/json; charset=utf-8");try {claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
} catch (ExpiredJwtException e) {log.debug("解析JWT失败:{}:{}", e.getClass().getName(), e.getMessage());Integer serviceCode = ServiceCode.ERR_JWT_EXPIRED.getValue();String message = "登录信息已过期,请重新登录!";JsonResult<Void> jsonResult = JsonResult.fail(serviceCode, message);String jsonString = JSON.toJSONString(jsonResult);PrintWriter writer = response.getWriter();writer.println(jsonString);writer.close();return;
} catch (SignatureException e) {log.debug("解析JWT失败:{}:{}", e.getClass().getName(), e.getMessage());Integer serviceCode = ServiceCode.ERR_JWT_PARSE.getValue();String message = "无法获取到有效的登录信息,请重新登录!";JsonResult<Void> jsonResult = JsonResult.fail(serviceCode, message);String jsonString = JSON.toJSONString(jsonResult);PrintWriter writer = response.getWriter();writer.println(jsonString);writer.close();return;
} catch (MalformedJwtException e) {log.debug("解析JWT失败:{}:{}", e.getClass().getName(), e.getMessage());Integer serviceCode = ServiceCode.ERR_JWT_PARSE.getValue();String message = "无法获取到有效的登录信息,请重新登录!";JsonResult<Void> jsonResult = JsonResult.fail(serviceCode, message);String jsonString = JSON.toJSONString(jsonResult);PrintWriter writer = response.getWriter();writer.println(jsonString);writer.close();return;
} catch (Throwable e) {log.debug("解析JWT失败:{}:{}", e.getClass().getName(), e.getMessage());Integer serviceCode = ServiceCode.ERR_JWT_PARSE.getValue();String message = "无法获取到有效的登录信息,请重新登录!";JsonResult<Void> jsonResult = JsonResult.fail(serviceCode, message);String jsonString = JSON.toJSONString(jsonResult);PrintWriter writer = response.getWriter();writer.println(jsonString);writer.close();e.printStackTrace();return;
}

6. 结合前端的登录页面

目前,后端的登录功能,如果成功登录,响应:

{"state": 20000,"message": null,"data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZXhwIjoxNjYyNzEyNDMxLCJhdXRob3JpdGllcyI6Ilt7XCJhdXRob3JpdHlcIjpcIi9hbXMvYWRtaW4vZGVsZXRlXCJ9LHtcImF1dGhvcml0eVwiOlwiL2Ftcy9hZG1pbi9yZWFkXCJ9LHtcImF1dGhvcml0eVwiOlwiL2Ftcy9hZG1pbi91cGRhdGVcIn0se1wiYXV0aG9yaXR5XCI6XCIvcG1zL3Byb2R1Y3QvZGVsZXRlXCJ9LHtcImF1dGhvcml0eVwiOlwiL3Btcy9wcm9kdWN0L3JlYWRcIn0se1wiYXV0aG9yaXR5XCI6XCIvcG1zL3Byb2R1Y3QvdXBkYXRlXCJ9XSIsInVzZXJuYW1lIjoicm9vdCJ9.VRK8btzrHmwU7gQ7Hu0-6nLYPYvh6-KlXSBTVH2NjAE"
}

如果用户名错误,响应:

{"state": 40100,"message": "登录失败,用户名或密码错误!","data": null
}

如果密码错误,响应:

{"state": 40100,"message": "登录失败,用户名或密码错误!","data": null
}

如果账号被禁用,响应:

{"state": 40110,"message": "登录失败,此账号已经禁用!","data": null
}

所以,在前端的登录页面提交请求:

onSubmit(formName) {this.$refs[formName].validate((valid) => {if (valid) {let url = 'http://localhost:9081/admins/login';console.log('请求路径:' + url);console.log('请求参数:');console.log(this.form);let formData = this.qs.stringify(this.form);this.axios.post(url, formData).then((response) => {console.log('服务器端的响应:');console.log(response);let responseBody = response.data;if (responseBody.state == 20000) {// 登录成功,服务器端将响应JWTlet jwt = responseBody.data;console.log('登录成功,服务器端响应的JWT:' + jwt);// 使用LocalStorage存储JWTlocalStorage.setItem('jwt', jwt);console.log('将JWT数据保存到LocalStorage!');// 测试:从LocalStorage中取出JWTlet localJwt = localStorage.getItem('jwt');console.log('测试:从LocalStorage中取出JWT:' + localJwt);// 提示this.$message({message: '登录成功!(暂不跳转)',type: 'success'});} else {// 用户名或密码错误、账号被禁用this.$message.error(responseBody.message);}});} else {alert('参数格式有误,不允许提交!');return false;}});
}

在后续的访问中,应该携带JWT再提交请求,例如:

loadAdminList() {console.log('准备从服务器获取管理员列表……');let jwt = localStorage.getItem('jwt'); // 新增console.log('从LocalStorage中取出JWT:' + jwt); // 新增let url = 'http://localhost:9081/admins';this.axios.create({'headers': {'Authorization': jwt}}) // 新增.get(url).then((response) => {console.log('服务器端响应的结果:')console.log(response);let responseBody = response.data;if (responseBody.state == 20000) {this.tableData = responseBody.data;} else {this.$message.error(responseBody.message);}});
}

7. 关于Spring Security的跨域与PreFlight

PreFlight:预检

当客户端提交异步请求时,如果自定义了非常规的请求头,则此请求会被视为“复杂请求”,会触发PreFlight(预检)机制!

当触发PreFlight时,客户端会自动提交一个OPTIONS类型的请求到服务器端,如果服务器端没有对此请求放行,则会出现403错误!

所以,可以选择:

解决方案1:使得Spring Security对所有OPTIONS请求放行:

http.authorizeRequests().antMatchers(urls).permitAll().antMatchers(HttpMethod.OPTIONS,"/**") // 新增.permitAll() // 新增.anyRequest().authenticated();

解决方案2:使用Spring Security的CorsFilter

// 允许跨域访问
http.cors(); // 激活Spring Security框架内置的一个CorsFilter,允许跨域访问

另外,需要注意:浏览器会对预检结果进行缓存,一旦通过预检,后续的每一次请求将不再执行预检!

作业

【作业1】在product项目完成以下功能,要求Mapper层、Service层、Controller层的代码开发,其中,Mapper层、Service层需要有对应的测试类,Controller层配置好在线API文档,关于Spring Validation的使用是可选的

  1. 添加属性模板(addNew)(见pms_attribute_template表),业务规则:属性模板名必须唯一
  2. 根据id删除属性模板(delete)(见pms_attribute_template表),业务规则:数据必须存在,不存在关联的属性(见pms_attribute表),不存在关联的类别(见pms_category_attribute_template表),不存在关联的SPU(见pms_spu表)
  3. 根据id修改属性模板基本资料(update)(见pms_attribute_template表),基本资料对应的属性:自行设计,业务规则:数据必须存在
  4. 添加属性(addNew)(见pms_attribute表),业务规则:同一个属性模板下的属性名必须唯一
  5. 根据id删除属性(delete),业务规则:数据必须存在
  6. 根据id修改属性基本资料(update),基本资料对应的属性:自行设计,业务规则:数据必须存在

【作业2】在前端项目中完成如下视图设计(不必实现与后端的交互,但页面内容必须完整且不报错)

  1. 显示品牌列表
  2. 添加属性页面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UQszQF7f-1663244838612)(images/image-20220909175332262.png)]


http://www.ppmy.cn/news/618744.html

相关文章

全栈之初识 Passport Passport-jwt – Web安全的守护神

一、Passport 简介 passport.js是Nodejs中的一个做登录验证的中间件&#xff0c;极其灵活和模块化&#xff0c;并且可与Express、Sails等Web框架无缝集成。Passport功能单一&#xff0c;即只能做登录验证&#xff0c;但非常强大&#xff0c;支持本地账号验证和第三方账号登录验…

【详细】使用 passport.js 来完成登录验证

使用 passport.js 完成后台验证 转载自楼主个人博客 使用 passport.js 来完成登录验证 - 2016/6/22 先啰嗦一段背景 介绍一下项目所使用的技术栈。Node.js&#xff0c;使用 Express 来完成后端服务器的架构&#xff0c;这个时候就遇到了一个问题了。在我以前&#xff0c;是用 J…

Laravel 的 API 认证系统 Passport 三部曲(二、passport的具体使用)

GQ1994 关注 2018.04.20 09:31 字数 1152 阅读 1316评论 0喜欢 1 参考链接 Laravel 的 API 认证系统 Passport 三部曲(一、passport安装配置) Laravel 的 API 认证系统 Passport 引言 在使用前要先了解Auth2.0的使用方式和原理Laravel 的用户认证系统passport是专门做api令牌授…

Nodejs Passport 系列之四:Passport 源码剖析之 OAuth2 认证流程

前言 本文是笔者所总结的有关 Nodejs Passport 系列之一&#xff1b;本文将从源码分析的角度&#xff0c;来深入剖析 passport 的认证流程&#xff1b; 本文为作者原创作品&#xff0c;转载请注明出处&#xff1b; 综述 OAuth2orize 包模块扩展使得 Express 成为具备 OAuth…

建立无需build的react单页面SPA框架

vue、react这种前端渲染的框架&#xff0c;比较适合做SPA。如果用ejs做SPA&#xff08;Single Page Application&#xff09;&#xff0c;js代码控制好全局变量冲突不算严重&#xff0c;但dom元素用jquery操作会遇到很多的名称上的冲突&#xff08;tag、id、name&#xff09;。…

DAY34——贪心part3

1. class Solution {public int largestSumAfterKNegations(int[] nums, int K) {// 将数组按照绝对值大小从大到小排序&#xff0c;注意要按照绝对值的大小nums IntStream.of(nums).boxed().sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1)).mapToInt(Integer::intValue)…

乱世王者服务器维护,乱世王者千变万化开服时间表_乱世王者新区开服预告_第一手游网手游开服表...

2021-07-16 11:00 微信641区虎啸风生 已经开服 2021-07-10 11:00 微信640区霸王别姬 已经开服 2021-07-05 11:00 微信639区无名英雄 已经开服 2021-07-02 11:00 微信638区予智予雄 已经开服 2021-06-29 11:00 微信637区一身正气 已经开服 2021-06-26 11:00 微信636区巾帼英雄 已…

使用PCA可视化数据

作者|Conor OSullivan 编译|VK 来源|Towards Data Science 主成分分析&#xff08;PCA&#xff09;是一个很好的工具&#xff0c;可以用来降低特征空间的维数。PCA的显著优点是它能产生不相关的特征&#xff0c;并能提高模型的性能。 它可以帮助你深入了解数据的分类能力。在…