项目实战(依旧还是登录认证,JWT解析异常处理,授权信息处理)

54. 关于SecurityContext中的认证信息

Spring Security框架是根据SecurityContext中是否存在认证信息来判断用户是否已经登录。

关于SecurityContext,是通过ThreadLocal进行处理的,所以,是线程安全的,每个客户端对应的SecurityContext中的信息是互不干扰的。

另外,SecurityContext中的认证信息是通过Session存储的,所以,一旦向SecurityContext中存入了认证信息,在后续一段时间(Session的有效时间)的访问中,即使不携带JWT,也是允许访问的,会被视为“已登录”。如果认为这样的表现是不安全的,可以在JWT过滤器中,在刚刚接收到请求时,就直接清除SecurityContext中的信息(主要是认证信息):

// 清除SecurityContext中原有的数据(认证信息)
SecurityContextHolder.clearContext();

53. 自定义配置

在处理JWT时,无论是生成JWT,还是解析JWT,都需要使用同一个secretKey,则应该将此secretKey定义在某个类中作为静态常量,或定义在配置文件(application.yml或等效的配置文件)中,由于此值是允许被软件的使用者(甲方)自行定义的,所以,更推荐定义在配置文件中。

则在application-dev.yml中添加自定义配置:

# 自定义配置
csmall:
  jwt:
    secret-key: kns439a}fdLK34jsmfd{MF5-8DJSsLKhJNFDSjn

提示:在配置文件中的自定义属性,应该在属性名称上添加统一的、自定义的前缀,例如以上使用到的csmall,以便于与其它的属性区分开来。

接下来,可以在需要使用以上配置值的类中,通过@Value注解将以上配置值注入到某个全局属性中,例如:

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

提示:以上使用的@Value注解可以读取当前项目中的全部环境变量,将包括:操作系统的环境变量、JVM的环境变量、各配置文件中的配置。并且,@Value注解可以添加在全局属性上,也可以添加在被Spring自动调用的方法的参数上。

54. 处理解析JWT时的异常

在JWT过滤器中,解析JWT时可能会出现异常,异常的类型主要有:

  • SignatureException
  • MalformedJwtException
  • ExpiredJwtException

由于解析JWT是发生成过滤器中的,而过滤器是整个Java EE体系中最早接收到请求的组件(此时,控制器等其它组件均未开始执行),所以,此时出现的异常不可以使用Spring MVC的全局异常处理器进行处理。

提示:Spring MVC的全局异常处理器在控制器(Controller)抛出异常之后执行。

只能通过最原始的try...catch...语法捕获并处理异常,处理时,需要使用到过滤器方法的第2个参数HttpServletResponse response来向客户端响应错误信息。

为了便于封装错误信息,应该使用JsonResult来封装相关信息,由于需要自行将JsonResult格式的对象转换成JSON格式的数据,所以,需要在pom.xml添加能够实现对象与JSON格式字符串相互转换的依赖,例如可以添加fastjson依赖:

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

然后,在ServiceCode中添加一些新的业务状态码:

public enum ServiceCode {

    // 前序代码

    ERR_JWT_SIGNATURE(60000),
    ERR_JWT_MALFORMED(60000),
    ERR_JWT_EXPIRED(60002),
    ERR_UNKNOWN(99999);

    // 后续代码

}

再开始处理异常,例如:

// 尝试解析JWT
log.debug("将尝试解析JWT……");
Claims claims = null;
try {
    claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
} catch (SignatureException e) {
    String message = "非法访问!";
    JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_SIGNATURE, message);
    String jsonResultString = JSON.toJSONString(jsonResult);
    PrintWriter writer = response.getWriter();
    writer.println(jsonResultString);
    writer.close();
    return;
} catch (MalformedJwtException e) {
    String message = "非法访问!";
    JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_MALFORMED, message);
    String jsonResultString = JSON.toJSONString(jsonResult);
    PrintWriter writer = response.getWriter();
    writer.println(jsonResultString);
    writer.close();
    return;
} catch (ExpiredJwtException e) {
    String message = "登录已过期,请重新登录!";
    JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_EXPIRED, message);
    String jsonResultString = JSON.toJSONString(jsonResult);
    PrintWriter writer = response.getWriter();
    writer.println(jsonResultString);
    writer.close();
    return;
} catch (Throwable e) {
    e.printStackTrace(); // 重要
    String message = "服务器忙,请稍后再次尝试!";
    JsonResult jsonResult = JsonResult.fail(ServiceCode.ERR_UNKNOWN, message);
    String jsonResultString = JSON.toJSONString(jsonResult);
    PrintWriter writer = response.getWriter();
    writer.println(jsonResultString);
    writer.close();
    return;
}

注意:强烈推荐在最后补充处理Throwable异常,以避免某些异常未被考虑到,并且,在处理Throwable时,应该执行e.printStackTrace(),则出现未预测的异常时,可以通过控制台看到相关信息,并在后续补充对这些异常的精准处理!

55. 处理授权

首先,需要调整现有的AdminMapper接口中的AdminLoginInfoVO getLoginInfoByUsername(String username)方法,此方法应该返回参数用户名匹配的管理员信息,信息中应该包含权限!

则需要执行的SQL语句大致是:

SELECT
    ams_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
    username='root';

则需要调整AdminLoginInfoVO类,添加新的属性,用于封装查询到的权限信息:

private List<String> permissions;

然后调整AdminMapper.xml中的相关配置:

<!-- AdminLoginInfoVO getLoginInfoByUsername(String username); -->
<select id="getLoginInfoByUsername" resultMap="LoginResultMap">
    SELECT
        <include refid="LoginQueryFields"/>
    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
        username=#{username}
</select>

<sql id="LoginQueryFields">
    <if test="true">
        ams_admin.id,
        ams_admin.username,
        ams_admin.password,
        ams_admin.enable,
        ams_permission.value
    </if>
</sql>

<!-- 当涉及1个多查询时,需要使用collection标签配置List集合类型的属性 -->
<!-- collection标签的property属性:类中List集合的属性的名称 -->
<!-- collection标签的ofType属性:类中List集合的元素类型的全限定名 -->
<!-- collection标签的子级:需要配置如何创建出一个个元素对象 -->
<!-- constructor标签:将通过构造方法来创建对象 -->
<!-- constructor标签子级的arg标签:配置构造方法的参数 -->
<resultMap id="LoginResultMap" type="cn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO">
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="password" property="password"/>
    <result column="enable" property="enable"/>
    <collection property="permissions" ofType="java.lang.String">
        <constructor>
            <arg column="value"/>
        </constructor>
    </collection>
</resultMap>

完成后,可以通过AdminMapperTests中原有的测试方法直接测试,测试结果例如:

根据username=fanchuanqi查询登录信息完成,结果=AdminLoginInfoVO(id=5, username=fanchuanqi, password=$2a$10$N.ZOn9G6/YLFixAOPMg/h.z7pCu6v2XyFDtC4q.jeeGm/TEZyj15C, enable=0, permissions=[/pms/picture/read, /pms/picture/add-new, /pms/picture/delete, /pms/picture/update, /pms/album/read, /pms/album/add-new, /pms/album/delete, /pms/album/update])

接下来,在UserDetailsServiceImpl中,向返回的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) {
        log.debug("此用户名【{}】不存在,即将抛出异常");
        String message = "登录失败,用户名不存在!";
        throw new BadCredentialsException(message);
    }

    // ===== 以下是此次调整的内容 =====
    List<GrantedAuthority> authorities = new ArrayList<>();
    for (String permission : loginInfo.getPermissions()) {
        GrantedAuthority authority = new SimpleGrantedAuthority(permission);
        authorities.add(authority);
    }

    AdminDetails adminDetails = new AdminDetails(
            loginInfo.getUsername(), loginInfo.getPassword(),
            loginInfo.getEnable() == 1, authorities);
    adminDetails.setId(loginInfo.getId());
}

经过以上调整后,在AdminServiceImpl处理登录的login()方法中,认证返回的结果的当事人(Principal)中就包含管理员的权限信息了!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mVG0hBTq-1667124989429)(file://C:\Users\lenovo\Desktop\第四阶段\doc\note\images\DAY16\image-20221014163109305.png)]

AdminServiceImpl类中的login()方法中,当认证成功后,得到的认证信息中的当事人信息就包含以上返回的AdminDetails,也就包含了管理员的权限信息,需要将此权限信息转换为JSON字符串(如果不转换,则后续解析时不便于还原出原始数据)并用于生成JWT数据:

log.debug("准备生成JWT数据");
Map<String, Object> claims = new HashMap<>();
claims.put("id", adminDetails.getId()); // 向JWT中封装id
claims.put("username", adminDetails.getUsername()); // 向JWT中封装username
// ===== 以下这1条语句是新增的 =====
claims.put("authorities", JSON.toJSONString(adminDetails.getAuthorities())); // 向JWT中封装权限

在JWT过滤器中,在解析JWT时,可以从JWT中得到权限的JSON字符串,应该将其转换成List<SimpleGrantedAuthority>并存入到认证信息中:

// 从JWT中获取用户的相关数据,例如id、username等
Long id = claims.get("id", Long.class);
String username = claims.get("username", String.class);
String authoritiesJsonString = claims.get("authorities", String.class); // 【调整】
log.debug("从JWT中解析得到数据:id={}", id);
log.debug("从JWT中解析得到数据:username={}", username);
log.debug("从JWT中解析得到数据:authoritiesJsonString={}", authoritiesJsonString);

// 准备用于创建认证信息的权限数据
List<SimpleGrantedAuthority> authorities
        = JSON.parseArray(authoritiesJsonString, SimpleGrantedAuthority.class); // 【调整】

// 准备用于创建认证信息的当事人数据
LoginPrincipal loginPrincipal = new LoginPrincipal();
loginPrincipal.setId(id);
loginPrincipal.setUsername(username);

// 创建认证信息
Authentication authentication = new UsernamePasswordAuthenticationToken(
        loginPrincipal, null, authorities);

// 将认证信息存储到SecurityContext中
log.debug("即将向SecurityContext中存入认证信息:{}", authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);

至此,关于登录相关的处理已经全部结束!

接下来,就可以配置各请求所需的权限,以实现对管理员权限的控制!

需要先在配置类上添加@EnableGlobalMethodSecurity(prePostEnabled = true)开启在方法上使用注解配置权限的功能,则在SecurityConfiguration类中添加此注解配置:

@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 新增
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    // 暂不关心类中的代码
}

然后,在任何处理请求的方法上,通过@PreAuthorize注解来配置对应请求所需的权限,例如:

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

经过以上配置,则“查询管理员列表”功能是只有具备/ams/admin/read权限的管理员才允许访问的,如果当前JWT对应的管理员不具备此权限,则会出现AccessDeniedException,例如:

org.springframework.security.access.AccessDeniedException: 不允许访问
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Blazor是一种基于WebAssembly的新型Web开发框架,可以使用C#语言开发客户端应用程序。它提供了一种方便的方式来实现JWT认证授权。 在Blazor应用程序中,您可以使用ASP.NET Core Identity和JSON Web Token(JWT)来实现JWT认证授权。首先,您需要在ASP.NET Core应用程序中配置JWT认证服务。然后,您可以使用Identity提供的API来管理用户和角色,以及实现基于角色的授权策略。 接下来,您需要在Blazor组件中使用`[Authorize]`属性来标记需要授权才能访问的组件。这将要求用户登录,并检查他们是否具有访问该组件的权限。 最后,您可以使用JWT来验证用户身份,并根据用户的角色和权限来授权访问。 下面是一个示例,演示如何在Blazor应用程序中使用JWT认证授权: ``` @page "/myprotectedpage" @attribute [Authorize(Roles = "Admin")] <h1>Welcome to the protected page!</h1> @code { [Inject] private IAccessTokenProvider TokenProvider { get; set; } private async Task<string> GetAccessTokenAsync() { var tokenResult = await TokenProvider.RequestAccessToken(); if (tokenResult.TryGetToken(out var token)) { return token.Value; } else { return null; } } protected override async Task OnInitializedAsync() { var accessToken = await GetAccessTokenAsync(); if (accessToken != null) { // Verify the token and extract the user's claims var handler = new JwtSecurityTokenHandler(); var token = handler.ReadJwtToken(accessToken); var userId = token.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; var roles = token.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList(); // Check if the user has the required role if (!roles.Contains("Admin")) { NavigationManager.NavigateTo("/accessdenied"); } } else { NavigationManager.NavigateTo("/login"); } } } ``` 在上面的示例中,我们使用`[Authorize(Roles = "Admin")]`属性来标记需要“Admin”角色才能访问的组件。然后,我们使用`IAccessTokenProvider`来获取JWT访问令牌,并验证令牌以确定用户的身份和角色。如果用户没有所需的角色,我们将重定向到一个名为“accessdenied”的页面。 希望这个示例能够帮助您实现Blazor中的JWT认证授权

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

专注摸鱼的汪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值