Shiro + JWT + Spring Boot Restful 简易教程(1)

  • 使用用户登入密码对 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;

文末

我将这三次阿里面试的题目全部分专题整理出来,并附带上详细的答案解析,生成了一份PDF文档

  • 第一个要分享给大家的就是算法和数据结构

网易严选Java开发三面面经:HashMap+JVM+索引+消息队列

  • 第二个就是数据库的高频知识点与性能优化

网易严选Java开发三面面经:HashMap+JVM+索引+消息队列

  • 第三个则是并发编程(72个知识点学习)

网易严选Java开发三面面经:HashMap+JVM+索引+消息队列

  • 最后一个是各大JAVA架构专题的面试点+解析+我的一些学习的书籍资料

网易严选Java开发三面面经:HashMap+JVM+索引+消息队列

还有更多的Redis、MySQL、JVM、Kafka、微服务、Spring全家桶等学习笔记这里就不一一列举出来

实现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;

文末

我将这三次阿里面试的题目全部分专题整理出来,并附带上详细的答案解析,生成了一份PDF文档

  • 第一个要分享给大家的就是算法和数据结构

[外链图片转存中…(img-wOcux49A-1714446552714)]

  • 第二个就是数据库的高频知识点与性能优化

[外链图片转存中…(img-9cDijP7q-1714446552715)]

  • 第三个则是并发编程(72个知识点学习)

[外链图片转存中…(img-cU4NiGD3-1714446552715)]

  • 最后一个是各大JAVA架构专题的面试点+解析+我的一些学习的书籍资料

[外链图片转存中…(img-1AwBm451-1714446552716)]

还有更多的Redis、MySQL、JVM、Kafka、微服务、Spring全家桶等学习笔记这里就不一一列举出来

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值