序文
Shiro は JWT シリーズを統合し、主に核となるアイデア (JWTToken を hiro+redis に統合する方法) を記録します。
前回の記事では、JwtToken、JwtUtil、および JwtFilter を作成する必要があることを学びました。
この記事では主に hiro フレームワークで Jwt を設定する方法について説明します。
ps: この記事は主に核となるアイデアを記録することに重点を置いています。
1.ShiroConfigの設定
- コアフラグメントコード:
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/sys/login", "anon"); //登录接口排除
filterChainDefinitionMap.put("/", "anon");
// 添加自己的过滤器并且取名为jwt 核心部分
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
filterChainDefinitionMap.put("/**", "jwt");
// 未授权界面返回JSON
shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
shiroFilterFactoryBean.setLoginUrl("/sys/common/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
ShiroConfig で、filterMap
それに保存されるキーと値のペアを作成し("jwt", new JwtFilter())
、それを Fileters として設定し、shiroFilterFactoryBean
最後の put でfilterChainDefinitionMap
それをインターセプトします。
ダウト: この場所は変だと感じる人もいますが、何が違うのかわかりません。
回答: 元々は hiro が最後にありfilterChainDefinitionMap.put("/**", "authc");
、残りのリクエストは認証のためにログインする必要がありましたが、現在はカスタム リクエストに変更されておりfilterChainDefinitionMap.put("/**", "jwt");
、残りのリクエストはすべて JwtFilter クラスに渡されて処理されます。
2.ShiroRealmの設定
@Component
@Slf4j
public class ShiroRealm extends AuthorizingRealm {
// 下面这个两个类,这里就不给出具体内容了,作用在下面一目了然
@Autowired
@Lazy
private ISysUserService sysUserService;
@Autowired
@Lazy
private RedisUtil redisUtil;
/** 2.1 supports */
/** 2.2 认证 */
/** 2.3 校验token的有效性 */
/** 2.4 token刷新(续签) */
/** 2.5 授权 */
}
2.1のサポート
- コード:
/**
* 2.1 supports
* 必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
Realm によって提供されるインターフェイス サポートは、もともと AuthenticatingRealm によって実装されたことがわかります。
上の図では、AuthenticatingRealmgetAuthenticationTokenClass
メソッドのデフォルト値は UsernamePasswordToken.class で、xxx.isAssignableFrom
受信クラスを(ID 変換または拡張参照変換によって)オブジェクト表現型cls
に変換できるかどうかを判断するために使用されます。xxx
isAssignableFrom
は Class.java のメソッドであり、その説明は次のとおりです。
このクラス オブジェクトによって表されるクラスまたはインターフェイスが、指定されたクラス パラメーターによって表されるクラスまたはインターフェイスと同じであるか、あるいはこのクラスまたはインターフェイスのスーパークラスまたはスーパーインターフェイスであるかを判断します。存在する場合は true を返し、そうでない場合は false を返します。この Class オブジェクトがプリミティブ型を表す場合、このメソッドは、指定された Class パラメーターがまさにこの Class オブジェクトである場合は true を返し、それ以外の場合は false を返します。【百度翻訳者】
- 概要: 1. 本来はトークンのString型をメソッド(UsernamePasswordToken.class)内の型
に変換できるかどうかです。2. ここでは、トークンが JwtToken 型に属するかどうかを判定するメソッドを書き換え、本来のデフォルトの判定を使用しないようにします。getAuthenticationTokenClass
supports
return token instanceof JwtToken;
2.2 認証 (doGetAuthenticationInfo)
- コード:
/**
* 2.2 认证
* @param auth 用户身份信息 token
* @return 返回封装了用户信息的 AuthenticationInfo 实例
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
// 这里的AuthenticationToken是用 JwtToken重写的实现方法getPrincipal()/getCredentials()都返回token
String token = (String) auth.getCredentials();
if (token == null) {
log.info("————————身份认证失败——————————");
throw new AuthenticationException("token为空!");
}
// 校验token有效性
SysUser loginUser = this.checkUserTokenIsEffect(token);
return new SimpleAuthenticationInfo(loginUser, token, getName());
}
これを見ると、このコードが以前の hiro とはかなり異なっていることがわかります。[Shiro] SimpleAuthenticationInfo がパスワードのカスタム ShihiroRealm クラスによって与えられた doGetAuthenticationInfo メソッドを検証する方法を比較できます。ただし、変更はそれほど大きくありません。最初に続行してください。checkUserTokenIsEffect メソッドを実行します。
2.3 トークンの有効性を確認する(checkUserTokenIsEffect)
- コード:
/**
* 2.3 校验token的有效性
*
* @param token
*/
public SysUser checkUserTokenIsEffect(String token) throws AuthenticationException {
// 解码获得username,用于查询数据库
String username = JwtUtil.getUsername(token);
if (username == null) {
throw new AuthenticationException("token非法无效!");
}
// 查询用户信息
SysUser loginUser = new SysUser();
SysUser sysUser = sysUserService.getUserByName(username);
//判断账号是否存在
if (sysUser == null) {
throw new AuthenticationException("用户不存在!");
}
// 校验token是否超时失效 & 或者账号密码是否错误 核心部分
if (!jwtTokenRefresh(token, username, sysUser.getPassWord())) {
throw new AuthenticationException("Token失效请重新登录!");
}
// 判断用户状态
if (!"0".equals(sysUser.getDelFlag())) {
throw new AuthenticationException("账号已被删除,请联系管理员!");
}
// 复制对象,为什么要这么做?麻烦懂的大佬留言指教一下
BeanUtils.copyProperties(sysUser, loginUser);
return loginUser;
}
全体的な観点から見ると、この部分の一般的なロジックは前の hiro と似ていることがわかります。その類似点と相違点を見てみましょう。
- 同じ部分:
AuthenticationToken オブジェクトを使用してユーザー名 (アカウント) を取得し、ユーザー名に従ってデータベースにクエリを実行して、ユーザーの User オブジェクト (ユーザー アカウント、暗号化されたパスワード、ソルト値など) を取得します。
PS: これらのロジックは、新しいメソッド checkUserTokenIsEffect に抽象化されています。 - 相違点:
1. 以前は、AuthenticationToken はユーザー名とパスワードの情報を保存していましたが、現在はトークン文字列です。
2. 以前に比べてトークンの有効性をより考慮する必要があり(詳細は2.4 jwtTokenRefreshを参照)、よく耳にするトークンの更新が登場します。
2.4 トークンのリフレッシュ (jwtTokenRefresh)
前提条件の理解:
JWTToken リフレッシュ ライフ サイクル (ユーザーがオンラインで操作していた問題を解決し、トークンを無効にするため)
1. ログインに成功した後、ユーザーの JWT によって生成されたトークンを k と v としてキャッシュに保存します (k と v の値) v はこの時点では同じです) 2.
ユーザーが再度要求すると、JWTFilter のレイヤーごとの検証に合格した後、本人確認のために doGetAuthenticationInfo が入力されます 3. ユーザーが今回
JWTToken 値を要求し、まだライフサイクル内では、k と v が Token 値であるように再 PUT され、キャッシュ内のトークン値のライフサイクル時間が再計算されます (このとき、k と v の値は同じです)
4ユーザーの jwt リクエストによって生成されたトークン値がタイムアウトしたが、キャッシュ内に k に対応するトークンがまだ存在する場合は、ユーザーは操作していましたが、JWT トークンが無効であることを意味します。プログラムは JWTToken を再生成します。トークンに対応する k マッピングの v 値を取得し、v 値を上書きします。キャッシュのライフ サイクルが再計算されます。5. ユーザーが今回 jwt を要求すると、生成されたトークン値はタイムアウトになり、トークンに対応する k がありません
。キャッシュが存在しない場合は、ユーザー アカウントがタイムアウトの間アイドル状態であり、返されたユーザー情報が無効であることを意味します。再度ログインしてください。
6. 戻り値が true になるたびに、レスポンスのヘッダーに Authorization が設定され、Authorization によってマッピングされた v がキャッシュに対応する v 値になります。
7. 注: 現在のエンドが受信した応答のヘッダーの Authorization 値は保存され、将来リクエスト トークンとして使用されます。
参考ソリューション: https://blog.csdn.net/qq394829044/article/details/ 82763936
- コード:
/**
* 2.4 token刷新
*
* @param token
* @param userName
* @param passWord
* @return
*/
public boolean jwtTokenRefresh(String token, String userName, String passWord) {
// 定义前缀+token 为缓存中的key,得到对应的value(cacheToken)
String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
// 判断缓存中的token是否存在
if (cacheToken != null && !cacheToken.equals("") && !cacheToken.equals("null")) {
// 校验token有效性
// 缓存中存在,验证失败(JwtUtil.verify在上一篇文章中已经介绍)
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
// 重新sign,得到新的token
String newAuthorization = JwtUtil.sign(userName, passWord);
// 写入到缓存中,key不变,将value换成新的token
redisUtil.set("PREFIX_TOKEN" + token, newAuthorization);
// 设置超时时间【这里除以1000是因为设置时间单位为秒了】【一般续签的时间都会乘以2】
redisUtil.expire("PREFIX_TOKEN" + token, JwtUtil.EXPIRE_TIME / 1000);
// 缓存中存在,验证成功
} else {
// 上面的写法,与下面的相同
// 用户这次请求JWTToken值还在生命周期内,重新put新的生命周期时间(有效时间)
redisUtil.set("PREFIX_TOKEN" + token, cacheToken, JwtUtil.EXPIRE_TIME / 1000);
}
return true;
}
return false;
}
PS: RedisUtil と sysUserService は冒頭のコードですでに言及されており、使用されるメソッドはコード内で説明されています。
2.5 認可(doGetAuthorizationInfo)
- コード:
/**
* 2.5 授权
*
* @param principals token
* @return AuthorizationInfo 权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("————权限认证 [ roles、permissions]————");
SysUser sysUser = null;
String username = null;
if (principals != null) {
sysUser = (SysUser) principals.getPrimaryPrincipal();
username = sysUser.getUserName();
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 设置用户拥有的角色集合,比如“admin,test”
Set<String> roleSet = sysUserService.getUserRolesSet(username);
info.setRoles(roleSet);
// 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
Set<String> permissionSet = sysUserService.getUserPermissionsSet(username);
info.addStringPermissions(permissionSet);
return info;
}
- 注:
ここで使用する redis 依存関係は次のとおりですspring-boot-starter-data-redis
。上記のコードでは、sysUserService がユーザー ロールと権限セットをクエリします。@Cacheable(value = "用来指定缓存组件的名字", key = "缓存数据时使用的 key")
クエリされた情報をキャッシュに保存するには、これらのメソッドの実装クラスにこれらを追加する必要があります。
疑問: @Cacheable を使用する必要がありますか? Redis 操作を直接使用してキャッシュに参加することは可能ですか?
回答: 必ずしもそうとは限りません; はい。@Cacheable は Spring のアノテーションであり、多くのキャッシュメソッドを実装しています。詳細についてはSpringBoot キャッシュの @Cacheableを参照してください。
3. トークン実行のフローチャート
ここでは、以下の図のステップ 2 と 6 について説明します。
- ステップ 2: コントローラー層によって処理される 4 つのキャッシュされたトークン。テスト中、redis が使用されます。トークンが生成された後、トークンは redis に保存されます。
- ステップ 6: なぜステートレス ログインなのかと疑問に思う人もいるかもしれません。トークンを使用するのはステートレスログインのためであるのに、なぜ Redis キャッシュを組み合わせる必要があるのかと疑問に思う人もいるかもしれません。セッションとの違いは何ですか?
- 説明:
1.ステートフル ログイン: ユーザーが要求すると、ユーザーの情報はサーバー上にキャッシュされ (セッション キャッシュを使用)、Cookie はクライアント側に保存され、セッションはサーバー側に保存されます。
ステートレス ログイン: ユーザーの情報はサーバーには保存されず、トークンのみがクライアントに保存されます。
2. Redis を使用する目的は、その後の分散サポート、高い同時実行スケーラビリティ (クラスター モード)、良好なパフォーマンス、およびより柔軟な使用に対処することです。(私はそれについて学んだばかりで、まだ詳しくありません)
3. Redis はメモリに基づいており、データの非同期ストレージをサポートしているため、Redis のパフォーマンスは従来のセッション ストレージ方法よりも効率的です。パフォーマンスの違いに加えて、次のような柔軟性も向上します。
1. トークン + Redis スキームの場合、サーバーは Redis 内の対応するトークンをクリアできるため、指定されたユーザーはサーバー側でログアウトできます。
2. トークン+セッション+redis スキームは、セッションを使用してトークンを保存し、そのセッションを redis に保存します。これには、セッションに複数のトークンを保存できるため、同じアカウントが複数のデバイスでセッションを共有できるという利点があります。
セッション、Cookie、トークン、Redis の詳細説明
セッションの仕組みと分散セッション共有ソリューション - 説明:
4.簡単な仕上げ:
-
Old:
前回のログインは、コントローラー層のログイン インターフェイスで実行されsubject.login(token)
、その後 doGetAuthenticationInfo メソッドに対して実行されました。ここで、トークンは UsernamePasswordToken です。doGetAuthenticationInfo では、主にユーザー オブジェクト、パスワード、ソルト値がデータベースから検索され、レルム名がSimpleAuthenticationInfo オブジェクトに追加されます。検証するためのassertCredentialsMatchの情報に使用されます。 -
新規:
Jwt を統合した後、コントローラー層のログイン インターフェイスは実行されませんsubject.login(token)
が、トークンが生成されてフロントエンドに返されます。getSubject(request, response).login(jwtToken);
後続のすべてのリクエストについて、フロントエンドはヘッダーにトークンを置き、JwtFilter によってインターセプトされたすべてのリクエストは、このリクエストのログインを実現するために、executeLogin メソッドで実行されます (各リクエストは、コントローラー層のログインインターフェイスではなく、実際のログインコードを実行する必要があります。)、メソッドが実行された後、Reealm の doGetAuthenticationInfo メソッドが実行されます。これは主にトークンの検証または更新に使用されます (上記で紹介した checkUserTokenIsEffect や jwtTokenRefresh など)。