溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

用代碼實(shí)例詳解Shiro+JWT+SpringBoot

發(fā)布時(shí)間:2020-07-20 09:11:43 來(lái)源:億速云 閱讀:292 作者:小豬 欄目:編程語(yǔ)言

小編這次要用代碼實(shí)例詳解Shiro+JWT+SpringBoot,文章內(nèi)容豐富,感興趣的小伙伴可以來(lái)了解一下,希望大家閱讀完這篇文章之后能夠有所收獲。

1.Shiro的簡(jiǎn)介

Apache Shiro是一種功能強(qiáng)大且易于使用的Java安全框架,它執(zhí)行身份驗(yàn)證,授權(quán),加密和會(huì)話管理,可用于保護(hù) 從命令行應(yīng)用程序,移動(dòng)應(yīng)用程序到Web和企業(yè)應(yīng)用程序等應(yīng)用的安全。

用代碼實(shí)例詳解Shiro+JWT+SpringBoot

  • Authentication 身份認(rèn)證/登錄,驗(yàn)證用戶是不是擁有相應(yīng)的身份;
  • Authorization 授權(quán),即權(quán)限驗(yàn)證,驗(yàn)證某個(gè)已認(rèn)證的用戶是否擁有某個(gè)權(quán)限;即判斷用戶是否能做事情,常見(jiàn)的如:驗(yàn)證某個(gè)用戶是否擁有某個(gè)角色?;蛘呒?xì)粒度的驗(yàn)證某個(gè)用戶對(duì)某個(gè)資源是否具有某個(gè)權(quán)限;
  • Cryptography 安全數(shù)據(jù)加密,保護(hù)數(shù)據(jù)的安全性,如密碼加密存儲(chǔ)到數(shù)據(jù)庫(kù),而不是明文存儲(chǔ);
  • Session Management 會(huì)話管理,即用戶登錄后就是一次會(huì)話,在沒(méi)有退出之前,它的所有信息都在會(huì)話中;
  • Web Integration web系統(tǒng)集成
  • Interations 集成其它應(yīng)用,spring、緩存框架

從應(yīng)用程序角度的來(lái)觀察如何使用Shiro完成工作:

Subject:主體,代表了當(dāng)前“用戶”,這個(gè)用戶不一定是一個(gè)具體的人,與當(dāng)前應(yīng)用交互的任何東西都是Subject,如網(wǎng)絡(luò)爬蟲(chóng),機(jī)器人等;即一個(gè)抽象概念;所有Subject都綁定到SecurityManager,與Subject的所有交互都會(huì)委托給SecurityManager;可以把Subject認(rèn)為是一個(gè)門面;SecurityManager才是實(shí)際的執(zhí)行者;

SecurityManager:安全管理器;即所有與安全有關(guān)的操作都會(huì)與SecurityManager交互;且它管理著所有Subject;可以看出它是Shiro的核心,它負(fù)責(zé)與后邊介紹的其他組件進(jìn)行交互,如果學(xué)習(xí)過(guò)SpringMVC,你可以把它看成DispatcherServlet前端控制器;

Realm:域,Shiro從從Realm獲取安全數(shù)據(jù)(如用戶、角色、權(quán)限),就是說(shuō)SecurityManager要驗(yàn)證用戶身份,那么它需要從Realm獲取相應(yīng)的用戶進(jìn)行比較以確定用戶身份是否合法;也需要從Realm得到用戶相應(yīng)的角色/權(quán)限進(jìn)行驗(yàn)證用戶是否能進(jìn)行操作;可以把Realm看成DataSource,即安全數(shù)據(jù)源。

也就是說(shuō)對(duì)于我們而言,最簡(jiǎn)單的一個(gè)Shiro應(yīng)用:

1、應(yīng)用代碼通過(guò)Subject來(lái)進(jìn)行認(rèn)證和授權(quán),而Subject又委托給SecurityManager;

2、我們需要給Shiro的SecurityManager注入Realm,從而讓SecurityManager能得到合法的用戶及其權(quán)限進(jìn)行判斷。

2.Shiro + JWT + SpringBoot

1.導(dǎo)入依賴

<dependency>
 <groupId>org.apache.shiro</groupId>
 <artifactId>shiro-spring</artifactId>
 <version>1.4.1</version>
</dependency>
<dependency>
 <groupId>com.auth0</groupId>
 <artifactId>java-jwt</artifactId>
 <version>3.8.2</version>
</dependency>

2.配置JWT

public class JWTUtil {
 /**
 * 校驗(yàn) 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();
  verifier.verify(token);
  return true;
 } catch (Exception e) {
  log.info("token is invalid{}", e.getMessage());
  return false;
 }
 }

 public static String getUsername(HttpServletRequest request) {
 // 取token
 String token = request.getHeader("Authorization");
 return getUsername(UofferUtil.decryptToken(token));
 }
 /**
 * 從 token中獲取用戶名
 * @return token中包含的用戶名
 */
 public static String getUsername(String token) {
 try {
  DecodedJWT jwt = JWT.decode(token);
  return jwt.getClaim("username").asString();
 } catch (JWTDecodeException e) {
  log.error("error:{}", e.getMessage());
  return null;
 }
 }
 
 public static Integer getUserId(HttpServletRequest request) {
 // 取token
 String token = request.getHeader("Authorization");
 return getUserId(UofferUtil.decryptToken(token));
 }
 /**
 * 從 token中獲取用戶ID
 * @return token中包含的ID
 */
 public static Integer getUserId(String token) {
 try {
  DecodedJWT jwt = JWT.decode(token);
  return Integer.valueOf(jwt.getSubject());
 } catch (JWTDecodeException e) {
  log.error("error:{}", e.getMessage());
  return null;
 }
 }


 /**
 * 生成 token
 * @param username 用戶名
 * @param secret 用戶的密碼
 * @return token 加密的token
 */
 public static String sign(String username, String secret, Integer userId) {
 try {
  Map<String, Object> map = new HashMap<>();
  map.put("alg", "HS256");
  map.put("typ", "JWT");
  username = StringUtils.lowerCase(username);
  Algorithm algorithm = Algorithm.HMAC256(secret);
  return JWT.create()
   .withHeader(map)
   .withClaim("username", username)
   .withSubject(String.valueOf(userId))
   .withIssuedAt(new Date())
//   .withExpiresAt(date)
   .sign(algorithm);
 } catch (Exception e) {
  log.error("error:{}", e);
  return null;
 }
 }
}

3.配置Shiro

4.實(shí)現(xiàn)JWTToken

token自己已經(jīng)包含了用戶名等信息。

@Data
public class JWTToken implements AuthenticationToken {

 private static final long serialVersionUID = 1282057025599826155L;

 private String token;

 private String exipreAt;

 public JWTToken(String token) {
 this.token = token;
 }

 public JWTToken(String token, String exipreAt) {
 this.token = token;
 this.exipreAt = exipreAt;
 }

 @Override
 public Object getPrincipal() {
 return token;
 }

 @Override
 public Object getCredentials() {
 return token;
 }

}

5.實(shí)現(xiàn)Realm

自定義實(shí)現(xiàn) ShiroRealm,包含認(rèn)證和授權(quán)兩大模塊。

public class ShiroRealm extends AuthorizingRealm {

 @Resource
 private RedisUtil redisUtil;

 @Autowired
 private ISysUserService userService;

 @Autowired
 private ISysRoleService roleService;

 @Autowired
 private ISysMenuService menuService;

 // 必須重寫此方法,不然Shiro會(huì)報(bào)錯(cuò)
 @Override
 public boolean supports(AuthenticationToken token) {
 return token instanceof JWTToken;
 }

 /**
 * 只有當(dāng)需要檢測(cè)用戶權(quán)限的時(shí)候才會(huì)調(diào)用此方法
 * 授權(quán)模塊,獲取用戶角色和權(quán)限。
 * @param token token
 * @return AuthorizationInfo 權(quán)限信息
 */
 @Override
 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection token) {
 Integer userId = JWTUtil.getUserId(token.toString());

 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

 // 獲取用戶角色集
 Set<String> roleSet = roleService.selectRolePermissionByUserId(userId);
 simpleAuthorizationInfo.setRoles(roleSet);

 // 獲取用戶權(quán)限集
 Set<String> permissionSet = menuService.findUserPermissionsByUserId(userId);
 simpleAuthorizationInfo.setStringPermissions(permissionSet);
 return simpleAuthorizationInfo;
 }

 /**
 * 用戶認(rèn)證:編寫shiro判斷邏輯,進(jìn)行用戶認(rèn)證
 * @param authenticationToken 身份認(rèn)證 token
 * @return AuthenticationInfo 身份認(rèn)證信息
 * @throws AuthenticationException 認(rèn)證相關(guān)異常
 */
 @Override
 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
 // 這里的 token是從 JWTFilter 的 executeLogin 方法傳遞過(guò)來(lái)的,已經(jīng)經(jīng)過(guò)了解密
 String token = (String) authenticationToken.getCredentials();
 String encryptToken = UofferUtil.encryptToken(token); //加密token
 String username = JWTUtil.getUsername(token); //從token中獲取username
 Integer userId = JWTUtil.getUserId(token); //從token中獲取userId

 // 通過(guò)redis查看token是否過(guò)期
 HttpServletRequest request = HttpContextUtil.getHttpServletRequest();
 String ip = IPUtil.getIpAddr(request);
 String encryptTokenInRedis = redisUtil.get(Constant.RM_TOKEN_CACHE + encryptToken + StringPool.UNDERSCORE + ip);
 if (!token.equalsIgnoreCase(UofferUtil.decryptToken(encryptTokenInRedis))) {
  throw new AuthenticationException("token已經(jīng)過(guò)期");
 }

 // 如果找不到,說(shuō)明已經(jīng)失效
 if (StringUtils.isBlank(encryptTokenInRedis)) {
  throw new AuthenticationException("token已經(jīng)過(guò)期");
 }

 if (StringUtils.isBlank(username)) {
  throw new AuthenticationException("token校驗(yàn)不通過(guò)");
 }

 // 通過(guò)用戶id查詢用戶信息
 SysUser user = userService.getById(userId);

 if (user == null) {
  throw new AuthenticationException("用戶名或密碼錯(cuò)誤");
 }
 if (!JWTUtil.verify(token, username, user.getPassword())) {
  throw new AuthenticationException("token校驗(yàn)不通過(guò)");
 }
 return new SimpleAuthenticationInfo(token, token, "febs_shiro_realm");
 }
}

6.重寫Filter

所有的請(qǐng)求都會(huì)先經(jīng)過(guò) Filter,所以我們繼承官方的 BasicHttpAuthenticationFilter ,并且重寫鑒權(quán)的方法。

代碼的執(zhí)行流程 preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin 。

@Slf4j
public class JWTFilter extends BasicHttpAuthenticationFilter {

 private static final String TOKEN = "Authorization";

 private AntPathMatcher pathMatcher = new AntPathMatcher();

 /**
 * 對(duì)跨域提供支持
 */
 @Override
 protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
 HttpServletRequest httpServletRequest = (HttpServletRequest) request;
 HttpServletResponse httpServletResponse = (HttpServletResponse) response;
 httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
 httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
 httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
 // 跨域時(shí)會(huì)首先發(fā)送一個(gè) option請(qǐng)求,這里我們給 option請(qǐng)求直接返回正常狀態(tài)
 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
  httpServletResponse.setStatus(HttpStatus.OK.value());
  return false;
 }
 return super.preHandle(request, response);
 }
 
 @Override
 protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
 HttpServletRequest httpServletRequest = (HttpServletRequest) request;
 UofferProperties UofferProperties = SpringContextUtil.getBean(UofferProperties.class);
 // 獲取免認(rèn)證接口 url 
 // 在application.yml中配置/adminApi/auth/doLogin/**,/adminApi/auth/register/**, ...
 String[] anonUrl = StringUtils.splitByWholeSeparatorPreserveAllTokens(UofferProperties.getShiro().getAnonUrl(), ",");

 boolean match = false;
 for (String u : anonUrl) {
  if (pathMatcher.match(u, httpServletRequest.getRequestURI())) {
  match = true;
  }
 }
 if (match) {
  return true;
 }
 if (isLoginAttempt(request, response)) {
  return executeLogin(request, response);
 }
 return false;
 }

 /**
 * 判斷用戶是否想要登入。
 * 檢測(cè)header里面是否包含Authorization字段即可
 */
 @Override
 protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
 HttpServletRequest req = (HttpServletRequest) request;
 String token = req.getHeader(TOKEN);
 return token != null;
 }

 @Override
 protected boolean executeLogin(ServletRequest request, ServletResponse response) {
 HttpServletRequest httpServletRequest = (HttpServletRequest) request;
 String token = httpServletRequest.getHeader(TOKEN); //得到token
 JWTToken jwtToken = new JWTToken(UofferUtil.decryptToken(token)); // 解密token
 try {
  // 提交給realm進(jìn)行登入,如果錯(cuò)誤他會(huì)拋出異常并被捕獲
  getSubject(request, response).login(jwtToken);
  // 如果沒(méi)有拋出異常則代表登入成功,返回true
  return true;
 } catch (Exception e) {
  log.error(e.getMessage());
  return false;
 }
 }

 @Override
 protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
 log.debug("Authentication required: sending 401 Authentication challenge response.");
 HttpServletResponse httpResponse = WebUtils.toHttp(response);
// httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
 httpResponse.setCharacterEncoding("utf-8");
 httpResponse.setContentType("application/json; charset=utf-8");
 final String message = "未認(rèn)證,請(qǐng)?jiān)谇岸讼到y(tǒng)進(jìn)行認(rèn)證";
 final Integer status = 401;
 try (PrintWriter out = httpResponse.getWriter()) {
//  String responseJson = "{\"message\":\"" + message + "\"}";
  JSONObject responseJson = new JSONObject();
  responseJson.put("msg", message);
  responseJson.put("status", status);
  out.print(responseJson);
 } catch (IOException e) {
  log.error("sendChallenge error:", e);
 }
 return false;
 }
}

7. ShiroConfig

@Configuration
public class ShiroConfig {

 @Bean
 public ShiroRealm shiroRealm() {
 // 配置 Realm
 return new ShiroRealm();
 }
 
 // 創(chuàng)建DefaultWebSecurityManager
 @Bean("securityManager")
 public SecurityManager securityManager() {
 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
 // 配置 SecurityManager,并注入 shiroRealm
 securityManager.setRealm(shiroRealm());
 return securityManager;
 }
 
 // 創(chuàng)建ShiroFilterFactoryBean
 @Bean
 public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
 
 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
 // 設(shè)置 securityManager
 shiroFilterFactoryBean.setSecurityManager(securityManager);

 	//添加Shiro過(guò)濾器
		/**
		 * Shiro內(nèi)置過(guò)濾器,可以實(shí)現(xiàn)權(quán)限相關(guān)的攔截器
		 * 常用的過(guò)濾器:
		 * anon: 無(wú)需認(rèn)證(登錄)可以訪問(wèn)
		 * authc: 必須認(rèn)證才可以訪問(wèn)
		 * user: 如果使用rememberMe的功能可以直接訪問(wèn)
		 * perms: 該資源必須得到資源權(quán)限才可以訪問(wèn)
		 * role: 該資源必須得到角色權(quán)限才可以訪問(wèn)
		 */
 
 // 在 Shiro過(guò)濾器鏈上加入 自定義過(guò)濾器JWTFilter 并取名為jwt
 LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
 filters.put("jwt", new JWTFilter());
 shiroFilterFactoryBean.setFilters(filters);

 // 自定義url規(guī)則
 LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
 // 所有請(qǐng)求都要經(jīng)過(guò) jwt過(guò)濾器
 filterChainDefinitionMap.put("/**", "jwt");
 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
 return shiroFilterFactoryBean;
 }

 /**
 * 下面的代碼是添加注解支持
 */
 @Bean
 @DependsOn({"lifecycleBeanPostProcessor"})
 public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
 // 設(shè)置代理類
 DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
 creator.setProxyTargetClass(true);

 return creator;
 }

 /**
 * 開(kāi)啟aop注解支持
 *
 * @param securityManager
 * @return
 */
 @Bean("authorizationAttributeSourceAdvisor")
 public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
 AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
 authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
 return authorizationAttributeSourceAdvisor;
 }

 
 // Shiro生命周期處理器
 @Bean
 public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
 return new LifecycleBeanPostProcessor();
 }

}

8.登陸

 /**
 * 登錄方法
 *
 * @param username 用戶名
 * @param password 密碼
 * @param code 驗(yàn)證碼
 * @param uuid 唯一標(biāo)識(shí)
 * @return 結(jié)果
 */
 @PostMapping("/doLogin")
 public ResultVo login(String username, String password, String code, String uuid, HttpServletRequest request) throws UofferException {

 String verifyKey = Constant.RM_CAPTCHA_CODE_KEY + uuid;
 String captcha = redisUtil.getCacheObject(verifyKey);
 redisUtil.del(verifyKey);

 if (captcha == null) {
  return ResultVo.failed(201, "驗(yàn)證碼失效");
 }
 if (!code.equalsIgnoreCase(captcha)) {
  return ResultVo.failed(201, "驗(yàn)證碼錯(cuò)誤");
 }

 username = StringUtils.lowerCase(username);
 password = MD5Util.encrypt(username, password);

 final String errorMessage = "用戶名或密碼錯(cuò)誤";
 SysUser user = userManager.getUser(username);

 if (user == null) {
  return ResultVo.failed(201, errorMessage);
 }
 if (!StringUtils.equalsIgnoreCase(user.getPassword(), password)) {
  return ResultVo.failed(201, errorMessage);
 }
 if (Constant.STATUS_LOCK.equals(user.getStatus())) {
  return ResultVo.failed(201, "賬號(hào)已被鎖定,請(qǐng)聯(lián)系管理員!");
 }


 Integer userId = user.getUserId();
 String ip = IPUtil.getIpAddr(request);
 String address = AddressUtil.getCityInfo(ip);
 // 更新用戶登錄時(shí)間
 SysUser sysUser = new SysUser();
 sysUser.setUserId(userId);
 sysUser.setLastLoginTime(new Date());
 sysUser.setLastLoginIp(ip);
 userService.updateById(sysUser);


 // 拿到token之后加密
 String sign = JWTUtil.sign(username, password, userId);
 String token = UofferUtil.encryptToken(sign);
 LocalDateTime expireTime = LocalDateTime.now().plusSeconds(properties.getShiro().getJwtTimeOut());
 String expireTimeStr = DateUtil.formatFullTime(expireTime);
 JWTToken jwtToken = new JWTToken(token, expireTimeStr);

 // 將登錄日志存入日志表中
 SysLoginLog loginLog = new SysLoginLog();
 loginLog.setIp(ip);
 loginLog.setAddress(address);
 loginLog.setLoginTime(new Date());
 loginLog.setUsername(username);
 loginLog.setUserId(userId);
 loginLogService.save(loginLog);

 saveTokenToRedis(username, jwtToken, ip, address);
 JSONObject data = new JSONObject();
 data.put("Authorization", token);

 // 將用戶配置及權(quán)限存入redis中
 userManager.loadOneUserRedisCache(userId);
 return ResultVo.oK(data);
 }

9.@RequiresPermissions

要求subject中必須含有bus:careerTalk:query的權(quán)限才能執(zhí)行方法someMethod()。否則拋出異常AuthorizationException。

@RequiresPermissions("bus:careerTalk:query")
public void someMethod() {
}

看完這篇關(guān)于用代碼實(shí)例詳解Shiro+JWT+SpringBoot的文章,如果覺(jué)得文章內(nèi)容寫得不錯(cuò)的話,可以把它分享出去給更多人看到。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI