Shiro + JWT + Spring Boot Restful 简易教程

  • 模拟 HTTP 请求工具,我使用的是 PostMan。

简要的说明下我们为什么要用 JWT ,因为我们要实现完全的前后端分离,所以不可能使用 sessioncookie 的方式进行鉴权,所以 JWT 就被派上了用场,你可以通过一个加密密钥来进行前后端的鉴权。

程序逻辑


  1. 我们 POST 用户名与密码到 /login 进行登入,如果成功返回一个加密 token,失败的话直接返回 401 错误。

  2. 之后用户访问每一个需要权限的网址请求必须在 header 中添加 Authorization 字段,例如 Authorization: tokentoken 为密钥。

  3. 后台会进行 token 的校验,如果有误会直接返回 401。

Token加密说明


  • 携带了 username 信息在 token 中。

  • 设定了过期时间。

  • 使用用户登入密码对 token 进行加密。

Token校验流程


  1. 获得 token 中携带的 username 信息。

  2. 进入数据库搜索这个用户,得到他的密码。

  3. 使用用户的密码来检验 token 是否正确。

准备Maven文件


新建一个 Maven 工程,添加相关的 dependencies。

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns=“http://maven.apache.org/POM/4.0.0”

xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”

xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd”>

4.0.0

org.inlighting

shiro-study

1.0-SNAPSHOT

org.apache.shiro

shiro-spring

1.3.2

com.auth0

java-jwt

3.2.0

org.springframework.boot

spring-boot-starter-web

1.5.8.RELEASE

org.springframework.boot

spring-boot-maven-plugin

1.5.7.RELEASE

repackage

org.apache.maven.plugins

maven-compiler-plugin

1.8

1.8

UTF-8

注意指定JDK版本和编码。

构建简易的数据源


为了缩减教程的代码,我使用 HashMap 本地模拟了一个数据库,结构如下:

| username | password | role | permission |

| — | — | — | — |

| smith | smith123 | user | view |

| danny | danny123 | admin | view,edit |

这是一个最简单的用户权限表,如果想更加进一步了解,自行百度 RBAC。

之后再构建一个 UserService 来模拟数据库查询,并且把结果放到 UserBean 之中。

UserService.java

@Component

public class UserService {

public UserBean getUser(String username) {

// 没有此用户直接返回null

if (! DataSource.getData().containsKey(username))

return null;

UserBean user = new UserBean();

Map<String, String> detail = DataSource.getData().get(username);

user.setUsername(username);

user.setPassword(detail.get(“password”));

user.setRole(detail.get(“role”));

user.setPermission(detail.get(“permission”));

return user;

}

}

UserBean.java

public class UserBean {

private String username;

private String password;

private String role;

private String permission;

public String getUsername() {

return username;

}

public void setUsername(String username) {

this.username = username;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

public String getRole() {

return role;

}

public void setRole(String role) {

this.role = role;

}

public String getPermission() {

return permission;

}

public void setPermission(String permission) {

this.permission = permission;

}

}

配置 JWT


我们写一个简单的 JWT 加密,校验工具,并且使用用户自己的密码充当加密密钥,这样保证了 token 即使被他人截获也无法破解。并且我们在 token 中附带了 username 信息,并且设置密钥5分钟就会过期。

public class JWTUtil {

// 过期时间5分钟

private static final long EXPIRE_TIME = 5601000;

/**

* 校验token是否正确

* @param token 密钥

* @param secret 用户的密码

* @return 是否正确

*/

public static boolean verify(String token, String username, String secret) {

try {

Algorithm algorithm = Algorithm.HMAC256(secret);

JWTVerifier verifier = JWT.require(algorithm)

.withClaim(“username”, username)

.build();

DecodedJWT jwt = verifier.verify(token);

return true;

} catch (Exception exception) {

return false;

}

}

/**

* 获得token中的信息无需secret解密也能获得

* @return token中包含的用户名

*/

public static String getUsername(String token) {

try {

DecodedJWT jwt = JWT.decode(token);

return jwt.getClaim(“username”).asString();

} catch (JWTDecodeException e) {

return null;

}

}

/**

* 生成签名,5min后过期

* @param username 用户名

* @param secret 用户的密码

* @return 加密的token

*/

public static String sign(String username, String secret) {

try {

Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);

Algorithm algorithm = Algorithm.HMAC256(secret);

// 附带username信息

return JWT.create()

.withClaim(“username”, username)

.withExpiresAt(date)

.sign(algorithm);

} catch (UnsupportedEncodingException e) {

return null;

}

}

}

构建URL


ResponseBean.java

既然想要实现 restful,那我们要保证每次返回的格式都是相同的,因此我建立了一个 ResponseBean 来统一返回的格式。(搜索公众号Java知音,回复“2021”,送你一份Java面试题宝典)

public class ResponseBean {

// http 状态码

private int code;

// 返回信息

private String msg;

// 返回的数据

private Object data;

public ResponseBean(int code, String msg, Object data) {

this.code = code;

this.msg = msg;

this.data = data;

}

public int getCode() {

return code;

}

public void setCode(int code) {

this.code = code;

}

public String getMsg() {

return msg;

}

public void setMsg(String msg) {

this.msg = msg;

}

public Object getData() {

return data;

}

public void setData(Object data) {

this.data = data;

}

}

自定义异常

为了实现我自己能够手动抛出异常,我自己写了一个 UnauthorizedException.java

public class UnauthorizedException extends RuntimeException {

public UnauthorizedException(String msg) {

super(msg);

}

public UnauthorizedException() {

super();

}

}

URL结构

| URL | 作用 |

| — | — |

| /login | 登入 |

| /article | 所有人都可以访问,但是用户与游客看到的内容不同 |

| /require_auth | 登入的用户才可以进行访问 |

| /require_role | admin的角色用户才可以登入 |

| /require_permission | 拥有view和edit权限的用户才可以访问 |

Controller

@RestController

public class WebController {

private static final Logger LOGGER = LogManager.getLogger(WebController.class);

private UserService userService;

@Autowired

public void setService(UserService userService) {

this.userService = userService;

}

@PostMapping(“/login”)

public ResponseBean login(@RequestParam(“username”) String username,

@RequestParam(“password”) String password) {

UserBean userBean = userService.getUser(username);

if (userBean.getPassword().equals(password)) {

return new ResponseBean(200, “Login success”, JWTUtil.sign(username, password));

} else {

throw new UnauthorizedException();

}

}

@GetMapping(“/article”)

public ResponseBean article() {

Subject subject = SecurityUtils.getSubject();

if (subject.isAuthenticated()) {

return new ResponseBean(200, “You are already logged in”, null);

} else {

return new ResponseBean(200, “You are guest”, null);

}

}

@GetMapping(“/require_auth”)

@RequiresAuthentication

public ResponseBean requireAuth() {

return new ResponseBean(200, “You are authenticated”, null);

}

@GetMapping(“/require_role”)

@RequiresRoles(“admin”)

public ResponseBean requireRole() {

return new ResponseBean(200, “You are visiting require_role”, null);

}

@GetMapping(“/require_permission”)

@RequiresPermissions(logical = Logical.AND, value = {“view”, “edit”})

public ResponseBean requirePermission() {

return new ResponseBean(200, “You are visiting permission require edit,view”, null);

}

@RequestMapping(path = “/401”)

@ResponseStatus(HttpStatus.UNAUTHORIZED)

public ResponseBean unauthorized() {

return new ResponseBean(401, “Unauthorized”, null);

}

}

处理框架异常

之前说过 restful 要统一返回的格式,所以我们也要全局处理 Spring Boot 的抛出异常。利用 @RestControllerAdvice 能很好的实现。

@RestControllerAdvice

public class ExceptionController {

// 捕捉shiro的异常

@ResponseStatus(HttpStatus.UNAUTHORIZED)

@ExceptionHandler(ShiroException.class)

public ResponseBean handle401(ShiroException e) {

return new ResponseBean(401, e.getMessage(), null);

}

// 捕捉UnauthorizedException

@ResponseStatus(HttpStatus.UNAUTHORIZED)

@ExceptionHandler(UnauthorizedException.class)

public ResponseBean handle401() {

return new ResponseBean(401, “Unauthorized”, null);

}

// 捕捉其他所有异常

@ExceptionHandler(Exception.class)

@ResponseStatus(HttpStatus.BAD_REQUEST)

public ResponseBean globalException(HttpServletRequest request, Throwable ex) {

return new ResponseBean(getStatus(request).value(), ex.getMessage(), null);

}

private HttpStatus getStatus(HttpServletRequest request) {

Integer statusCode = (Integer) request.getAttribute(“javax.servlet.error.status_code”);

if (statusCode == null) {

return HttpStatus.INTERNAL_SERVER_ERROR;

}

return HttpStatus.valueOf(statusCode);

}

}

配置 Shiro


大家可以先看下官方的 Spring-Shiro 整合教程,有个初步的了解。不过既然我们用了 Spring-Boot,那我们肯定要争取零配置文件。(搜索公众号Java知音,回复“2021”,送你一份Java面试题宝典)

实现JWTToken

JWTToken 差不多就是 Shiro 用户名密码的载体。因为我们是前后端分离,服务器无需保存用户状态,所以不需要 RememberMe 这类功能,我们简单的实现下 AuthenticationToken 接口即可。因为 token 自己已经包含了用户名等信息,所以这里我就弄了一个字段。如果你喜欢钻研,可以看看官方的 UsernamePasswordToken 是如何实现的。

public class JWTToken implements AuthenticationToken {

// 密钥

private String token;

public JWTToken(String token) {

this.token = token;

}

@Override

public Object getPrincipal() {

return token;

}

@Override

public Object getCredentials() {

return token;

}

}

实现Realm

realm 的用于处理用户是否合法的这一块,需要我们自己实现。

@Service

public class MyRealm extends AuthorizingRealm {

private static final Logger LOGGER = LogManager.getLogger(MyRealm.class);

private UserService userService;

@Autowired

public void setUserService(UserService userService) {

this.userService = userService;

}

/**

* 大坑!,必须重写此方法,不然Shiro会报错

*/

@Override

public boolean supports(AuthenticationToken token) {

return token instanceof JWTToken;

}

/**

* 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的

*/

@Override

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

String username = JWTUtil.getUsername(principals.toString());

UserBean user = userService.getUser(username);

SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

simpleAuthorizationInfo.addRole(user.getRole());

Set permission = new HashSet<>(Arrays.asList(user.getPermission().split(“,”)));

simpleAuthorizationInfo.addStringPermissions(permission);

return simpleAuthorizationInfo;

}

/**

* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。

*/

@Override

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {

String token = (String) auth.getCredentials();

// 解密获得username,用于和数据库进行对比

String username = JWTUtil.getUsername(token);

if (username == null) {

throw new AuthenticationException(“token invalid”);

}

UserBean userBean = userService.getUser(username);

if (userBean == null) {

throw new AuthenticationException(“User didn’t existed!”);

}

if (! JWTUtil.verify(token, username, userBean.getPassword())) {

throw new AuthenticationException(“Username or password error”);

}

return new SimpleAuthenticationInfo(token, token, “my_realm”);

}

}

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
username == null) {

throw new AuthenticationException(“token invalid”);

}

UserBean userBean = userService.getUser(username);

if (userBean == null) {

throw new AuthenticationException(“User didn’t existed!”);

}

if (! JWTUtil.verify(token, username, userBean.getPassword())) {

throw new AuthenticationException(“Username or password error”);

}

return new SimpleAuthenticationInfo(token, token, “my_realm”);

}

}

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-Q6ziAMK3-1714954947801)]

[外链图片转存中…(img-Bsq5RV89-1714954947802)]

[外链图片转存中…(img-fhSXca0p-1714954947802)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 29
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值