Shiro&JWT&権限管理
始める前に、2つの概念を理解しましょう
認証
認証プロセスで行うべき主なことは、訪問者が誰であるかを知ることです、彼は私たちのシステムに登録しましたか?彼は私たちのシステムのユーザーですか?はいの場合、このユーザーはブラックリストに登録されていますか?これが認証です。たとえば、観光地に入るときは、まずチケットを購入する必要があります。改札口にチケットがない場合は、叔父/叔母によって直接ブロックされます。今日チケットを購入しましたか?チケットは有効期限が切れていますか?チケットを持っていてすべてが正常であれば、入場できます。これは認定プロセスです!
認可
承認の役割は、特定のリソースにアクセスする権利があることを確認することです。MySQLデータベースでは、rootユーザーが各ライブラリのデータを追加、削除、変更、および確認できることは誰もが知っていますが、通常のユーザーには、表示する権限のみがあり、削除および変更する権限はありません。プログラムにも同じことが当てはまり、非常に一般的な要件があります。ユーザーAはページにアクセスして変更できます。ユーザーBはこのページにアクセスする権限しかなく、変更できないためです。
shiroとjwtの簡単な紹介
jwt:リクエストヘッダー、リクエストボディ、署名の3つの部分で構成されるJSON Webトークンで、主に認証に使用されます。
Shiroは、アノテーションを使用して権限管理をすばやく実装できる軽量のセキュリティフレームワークです。彼は主に認可に使用されます。
認証と承認を実現するための全体的なアイデア
クライアントはサーバーにアクセスし、サーバーはユーザー名とパスワードが正しいかどうかを含む要求を認証します。認証が成功した場合、資格情報トークンがクライアントに発行され、クライアントは後でサーバーにアクセスするときにこのトークンを保持します。または、トークンが改ざんされている場合、認証は失敗します!トークン認証が成功し、クライアントがリソースにアクセスする場合、サーバーはユーザーがこのリソースにアクセスする権利を持っているかどうかも確認します!このユーザーがこのリソースにアクセスする権限を持っていない場合、アクセスも失敗します!
データベーステーブルの設計
权限的认证和校验采用RBAC模型,总共有5张表,分别为user、user_role、role、role_permission和permission,
其中user_role和role_permission为中间表,user表和role表示一对多的关系,
role表和permission表也是一对多的关系。
コードの実装
1つ目はログインコードで、これは認証に使用されるコードです。ユーザーが正常に認証されると、トークンがクライアントに返されます。認証に失敗すると、クライアントはトークンを取得できなくなります。
ログイン
@RestController
@Slf4j
public class LoginController {
@Resource
private UserService userService;
@PostMapping("/login")
public ResponseBean login(@RequestBody JSONObject requestJson) {
log.info("用户登录");
String username = requestJson.getString("username");
String password = requestJson.getString("password");
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
throw new RuntimeException("用户名和密码不可以为空!");
}
// 从数据库中根据用户名查找该用户信息
UserDTO userDTO = userService.getByUseName(username);
// 获取盐
String salt = userDTO.getSalt();
// 原密码加密(通过username + salt作为盐)
String encodedPassword = ShiroKit.md5(password, username + salt);
if (null != userDTO && userDTO.getPassword().equals(encodedPassword)) {
return new ResponseBean(200, "Login success", JWTUtil.sign(username, encodedPassword));
} else {
throw new UnauthorizedException("用户名或者密码错误!");
}
}
}
JWTはトークンを生成します(検証付きの一部のツールクラス)
public class JWTUtil {
final static Logger logger = LogManager.getLogger(JWTUtil.class);
/**
* 过期时间30分钟
*/
private static final long EXPIRE_TIME = 30 * 60 * 1000;
/**
* 校验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 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;
}
}
/**
* 生成签名,30min后过期
*
* @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信息
// 建造者模式
String jwtString = JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
logger.debug(String.format("JWT:%s", jwtString));
return jwtString;
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
フィルター
ログインリクエストを除外すると、他のすべてのリクエストはリクエストの前に認証プロセスを通過する必要があります。
public class JWTFilter extends BasicHttpAuthenticationFilter {
private Logger LOGGER = LoggerFactory.getLogger(this.getClass());
/**
* 判断用户是否想要登入。
* true:是要登录
* 检测header里面是否包含Authorization字段即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader("Authorization");
return authorization != null;
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader("Authorization");
JWTToken token = new JWTToken(authorization);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 这里我们详细说明下为什么最终返回的都是true,即允许访问
* 例如我们提供一个地址 GET /article
* 登入用户和游客看到的内容是不同的
* 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
* 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
* 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
* 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
try {
executeLogin(request, response);
} catch (Exception e) {
response401(request, response);
}
}
return true;
}
/**
* 对跨域提供支持
*/
@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"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 将非法请求跳转到 /401
*/
private void response401(ServletRequest req, ServletResponse resp) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.sendRedirect("/401");
} catch (IOException e) {
LOGGER.error(e.getMessage());
}
}
}
MyRealmをカスタマイズする
カスタマイズされた権限とID認証ロジック:
@Configuration
public class MyRealm extends AuthorizingRealm {
@Resource
private 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());
// 根据用户名获取用户角色 权限信息
UserDTO user = userService.getByUseName(username);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 获取角色
List<String> roleNames = getRoleNameList(user.getRoleList());
for (String roleName : roleNames) {
simpleAuthorizationInfo.addRole(roleName);
}
// 获取权限
List<String> permissions = getPermissionList(user.getPermissionList());
simpleAuthorizationInfo.addStringPermissions(new HashSet<>(permissions));
return simpleAuthorizationInfo;
}
/**
* 获取权限
*
* @param permissionList
* @return
*/
private List<String> getPermissionList(List<Permission> permissionList) {
List<String> permissions = new ArrayList<>(permissionList.size());
for (Permission permission : permissionList) {
if (StringUtils.isNotBlank(permission.getPerCode())) {
permissions.add(permission.getPerCode());
}
}
return permissions;
}
/**
* 获取角色名称
*
* @param roleList
* @return
*/
private List<String> getRoleNameList(List<Role> roleList) {
List<String> roleNames = new ArrayList<>(roleList.size());
for (Role role : roleList) {
roleNames.add(role.getName());
}
return roleNames;
}
/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
*/
@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");
}
UserDTO userBean = userService.getByUseName(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");
}
}
MyRealm
カスタムレルムは、権限を取得するための鍵です。MyRealmは、ShiroフレームワークのAuthorizingRealm抽象クラスを継承し、次にdoGetAuthorizationInfoメソッドを書き換えます。このメソッドでは、最初にクライアントが保持するトークンからユーザー名を取得し、ユーザー名に基づいてデータベースでチェックしますユーザーが所有する役割と権限をリストし、役割と権限をそれぞれSimpleAuthorizationInfoオブジェクトに入れます。次のように:
// 获取角色
List<String> roleNames = getRoleNameList(user.getRoleList());
for (String roleName : roleNames) {
simpleAuthorizationInfo.addRole(roleName);
}
// 获取权限
List<String> permissions = getPermissionList(user.getPermissionList());
simpleAuthorizationInfo.addStringPermissions(new HashSet<>(permissions));
Shiro注解
1.RequiresAuthentication:
- この注釈でマークされたクラス、インスタンス、およびメソッドは、アクセスまたは呼び出されたときに現在のセッションで認証される必要があります。
2.RequiresGuest:
- アクセスまたは呼び出し時にこの注釈でマークされたクラス、インスタンス、およびメソッド。現在のサブジェクトは「強制」アイデンティティである可能性があり、認証する必要がないか、元のセッションにレコードがあります。
3.RequiresPermissions:
- 現在のサブジェクトには、アノテーションでマークされたメソッドを実行するための特定の特定の権限が必要です。現在のサブジェクトにそのような権限がない場合、メソッドは実行されません。
4.RequiresRoles:
- アノテーションでマークされたメソッドにアクセスするには、現在のサブジェクトが指定されたすべてのロールを持っている必要があります。サブジェクトが指定されたすべてのロールを同時に持たない場合、メソッドは実行されず、AuthorizationExceptionがスローされます。
5.RequiresUser:
- 注釈でマークされたクラス、インスタンス、およびメソッドにアクセスまたは呼び出すには、現在のサブジェクトがアプリケーションのユーザーである必要があります。
私たちは、RequiresRolesとRequiresPermissionsをよく使用します。
これらの注釈は次の順序で使用されます。
RequiresRoles-->RequiresPermissions-->RequiresAuthentication-->RequiresUser-->RequiresGuest
如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回
@RequiresRoles(value = {"admin","guest"},logical = Logical.OR)
这个注解的作用是携带的token中的角色必须是admin、guest中的一个
如果把logical = Logical.OR改成logical = Logical.AND,那么这个注解的作用就变成角色必须同时包含admin和guest了.
@RequiresPermissions(logical = Logical.OR, value = {"user:view", "user:edit"})
和@RequiresRoles类似,权限的的值放在value中,值之间的关系用Logical.OR和Logical.AND控制
コードテスト
ログイン
トークンを持たずにリソースにアクセスする
トークンによるアクセス
ゲストの役割が必要ですが、管理者トークンです
ゲスト権限ゲストトークン
不十分な権限
アクセスできる
プロジェクトの完全なコード
参考ブログ投稿