0. はじめに:
Spring Security は、Spring ファミリのセキュリティ管理フレームワークです。別のセキュリティ フレームワークである Roku と比較すると、Shiro よりも豊富な機能と豊富なコミュニティ リソースが提供されます。
一般に、中規模および大規模のプロジェクトはセキュリティ フレームワークとして SpringSecurity を使用します。 SpringSecurityよりもShiroの方が簡単に始めることができるため、Shiroを使用した小さなプロジェクトがたくさんあります。
一般的な Web アプリケーションには認証と認可が必要です。
認証: 現在システムにアクセスしているユーザーがこのシステムのユーザーであるかどうかを確認し、どのユーザーであるかを確認します。は
認可: 認証後、現在のユーザーに特定の操作を実行する権限があるかどうかを判断します。
そして、認証と認可は、セキュリティフレームワークとしての SpringSecurity の中核機能でもあります。
1. クイックスタート
1.1 準備作業
単純な SpringBoot プロジェクトを構築します
1.2SpringSecurityの紹介
SpringBoot プロジェクトで SpringSecurity を使用するには、依存関係を導入して入門レベルのケースを実装するだけです。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.0</version>
</dependency>
依存関係を導入した後、前のインターフェイスにアクセスしようとすると、SpringSecurity のデフォルトのログイン ページに自動的にジャンプします。デフォルトのユーザー名は user で、パスワードはコンソールに出力されます。
インターフェースにアクセスするには、ログインする必要があります。
2. 認証
2.1 ログイン認証プロセス
2.2 原理の予備調査
独自のログイン プロセスを実装する方法を知りたい場合は、まず入門ケースとして SpringSecurity プロセスを理解する必要があります。
2.2.1SpringSecurityの完了プロセス
SpringSecurity の原理は、実際にはさまざまな機能を提供するフィルターを含むフィルター チェーンです。ここで、スターターの例のフィルターを見てみましょう。
図にはコア フィルタのみが示されており、他の非コア フィルタは図に示されていません。
UsernamePasswordAuthenticationFilter: ログイン ページでユーザー名とパスワードを入力した後のログイン リクエストの処理を担当します。主に、初心者レベルのケースの認証作業を担当します。
ExceptionTranslationFilter: フィルター チェーンでスローされた AccessDeniedException および AuthenticationException を処理します。
FilterSecurityInterceptor:権限検証を担当するフィルタ。 (認可の責任者)
Debug を使用すると、現在のシステムの SpringSecurity フィルター チェーンに含まれるフィルターとその順序を確認できます。
2.2.2 認証プロセスの詳細説明
コンセプトのクイックチェック:
認証インターフェイス: その実装クラスは、現在システムにアクセスしているユーザーを表し、ユーザー関連の情報をカプセル化します。 AuthenticationManager インターフェイス: 認証方法を定義します。 Authentication
UserDetailsService インターフェイス: ユーザー固有のデータをロードするためのコア インターフェイス。ユーザー名に基づいてユーザー情報をクエリするメソッドを定義します。
UserDetails インターフェイス: 主要なユーザー情報を提供します。 UserDetailsService を通じてユーザー名に基づいて取得および処理されたユーザー情報は、UserDetails オブジェクトにカプセル化して返す必要があります。この情報は認証オブジェクトにカプセル化されます。
2.3 解決策
2.3.1 アイデア分析
ログイン
(1) カスタマイズされたログインインターフェース
認証のためにProviderManagerメソッドを呼び出し、認証が通ればjwtが生成されます。
ユーザー情報をredisに保存する
(2) UserDetailsServiceのカスタマイズ
この実装列でデータベースにクエリを実行します
チェック:
(1) jwt認証フィルターを定義する
トークンを取得する
トークンを解析してその中のユーザーIDを取得します
Redisからユーザー情報を取得する
SecurityContextHoder に保存
2.3.2 準備
まずはテーブルを作成しましょう
CREATE TABLE `sys_user` (
`id` bigint(20) UNSIGNED ZEROFILL NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用户名',
`nick_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '昵称',
`password` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '密码',
`status` char(1) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT '0' COMMENT '状态(0正常 1停用)',
`email` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '邮箱',
`phonenumber` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '手机号',
`sex` char(1) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '性别(0男 1女 2未知)',
`avatar` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '头像',
`user_type` char(1) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用户类型(0管理员 1普通用户)',
`create_by` bigint(20) NULL DEFAULT NULL COMMENT '创建问的用户id',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`updata_by` bigint(20) NULL DEFAULT NULL COMMENT '更新人',
`updata_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
`del_flag` int(11) UNSIGNED ZEROFILL NULL DEFAULT 00000000000 COMMENT '删除日志(0代表未删除,1 代表已删除)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (00000000000000000001, 'admin', '管理员', '{noop}123456', '0', NULL, NULL, NULL, NULL, '0', NULL, '2022-06-12 21:26:35', NULL, '2022-06-12 21:26:40', 00000000000);
SET FOREIGN_KEY_CHECKS = 1;
MybatisPlus と mysql ドライバーの依存関係の紹介
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
コアコードの実装
実装クラス UserDetailsService インターフェイスを作成し、その中のメソッドをオーバーライドします。ユーザー名を変更してデータベースからユーザー情報をクエリする
package security_token.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.catalina.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import security_token.domain.LoginUser;
import security_token.entity.SysUser;
import security_token.mapper.UserMapper;
import java.util.Objects;
@Service
public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
//TODO 查询用户信息
LambdaQueryWrapper<SysUser> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getUserName,username);
SysUser sysUser = userMapper.selectOne(queryWrapper);
//如果没有查询到用户信息就抛出异常
if(Objects.isNull(sysUser)){
throw new RuntimeException("用户名或者密码错误");
}
//TODO 查询对应的权限信息
//把数据封装成UserDetails返回
return new LoginUser(sysUser);
}
}
UserDetailsService メソッドの戻り値は UserDetails 型であるため、クラスを定義し、インターフェイスを実装し、その中にユーザー情報をカプセル化します。
package security_token.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import security_token.entity.SysUser;
import java.util.Collection;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private SysUser sysUser;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return sysUser.getPassword();
}
@Override
public String getUsername() {
return sysUser.getUserName();
}
//判断用户是否过期 为true代表不过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//代表是否超时
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 代表是否可用
@Override
public boolean isEnabled() {
return true;
}
}
注: テストする場合は、ユーザー データをユーザー テーブルに書き込む必要があります。ユーザー パスワードをクリア テキストで保存する場合は、パスワードの前に {noop} を追加する必要があります。
2.3.3.2 パスワード暗号化ストレージ
実際のプロジェクトでは、パスワードはデータベースに平文で保存されません。
デフォルトでは、PasswordEncoder が使用されます。これには、データベース内のパスワード形式が {id}password である必要があります。ID に基づいてパスワードの暗号化方式が決定されます。ただし、通常はこのアプローチは使用しません。したがって、passwordEncoder を置き換える必要があります。
通常、SpringSecurity が提供する BCryptPasswordEncoder を使用します。
BCryptPasswordEncoder オブジェクトを Spring コンテナに挿入するだけで済み、SpringSecurity はパスワード検証に PasswordEncoder を使用します。 SpringSecurity 構成クラスを定義できます。SpringSecurity では、この構成クラスが WebSecurityConfigurerAdapter を継承する必要があります。
@Configuration
public class SecurityConfig extends WebMvcConfigurer {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
テストタイプのテストでは、2 つのパスワードを同じに入力すると、与えられる暗号文が異なります。
@Test
public void TestBc(){
BCryptPasswordEncoder Encoder=new BCryptPasswordEncoder();
//输入密文的方法
String encode = Encoder.encode("123456");
String encode1 = Encoder.encode("123465");
System.out.println(encode);
System.out.println(encode1);
//校验的方法
String encode = Encoder.encode("123456");
System.out.println(Encoder.matches("123456", "$2a$10$BhrLLfznZFONDomNEVbsTuOrSi4K6hTMN6/nBaV.oe2Or0z/c4ZTq"));
}
//运行结果
$2a$10$BhrLLfznZFONDomNEVbsTuOrSi4K6hTMN6/nBaV.oe2Or0z/c4ZTq
$2a$10$P9JyxbNoaW6QARFuDo5kyOj8Q9Po2VRGnqEyC8WGj1ljJKhnmhlCC
2.3.3.3 ログインインターフェース
次に、ログイン インターフェイスをカスタマイズし、ユーザーがログインせずにこのインターフェイスにアクセスできるように、springSecurity にこのインターフェイスを解放させる必要があります。
インターフェイスでは、AuthenticationManage のAuthenticate メソッドを使用してユーザー認証を実行するため、SecurityConfig で AuthenticationManager がコンテナに挿入されるように構成する必要があります。
認証が成功すると、jwt が生成され、応答で返されます。そして、ユーザーが次のリクエストを行うときに jwt を通じて特定のユーザーを識別できるように、ユーザー情報を redis に保存する必要があり、ユーザー ID をキーとして使用できます。
@Service
public class LoginService {
@Autowired
private RedisCache redisCache;
@Autowired
private AuthenticationManager authenticationManager;
public Results<Map<String, String>> login(SysUser sysUser){
//AuthenticationManager authenticate进行用户认证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(sysUser.getUserName(),sysUser.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//认证没有通过,给出对应的提示
if (Objects.isNull(authenticate)){
throw new RuntimeException("登录失败");
}
//如果认证通过了,使用userid生成一个jwt,存入Results
LoginUser loginUser=(LoginUser) authenticate.getPrincipal();
String userId = loginUser.getSysUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
Map<String, String> map = new HashMap<>();
map.put("token",jwt);
//把完整的用户信息存入redis,userid作为id
redisCache.setCacheObject("login:"+userId,loginUser);
return Results.success(ResponseCode.SUCCESS,map);
}
}
2.3.3.4 認証フィルター
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String header = request.getHeader("token");
// StringUtils.hasText()作用
// 如果里面的值为null,""," ",那么返回值为false;否则为true
if (!StringUtils.hasText(header)){
//就是说header里面没有token,放行,并且结束执行后面的代码
filterChain.doFilter(request,response);
return;
}
String userId;
//解析token
try {
Claims claims = JwtUtil.parseJWT(header); userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey="login:"+userId;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
//存入securityContextHolder
//TODO 获取权限信息封装到Authenticate中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,
null,null);
SecurityContextHolder securityContextHolder = new SecurityContextHolder();
// 放行
filterChain.doFilter(request,response);
//FilterChain.doFilter(request,response)
}
}
2.3.3.5 ログアウト
//注销接口
public Results logout() {
//获取securityHolder中的用户id
UsernamePasswordAuthenticationToken authenticationToken=(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authenticationToken.getPrincipal();
Integer userId = loginUser.getSysUser().getId();
//删除redis中用户id
redisCache.deleteObject("login:"+userId);
return new Results(200,"注销成功");
}
3. 認可
3.0 許可システムの役割
例えば、学校図書館管理システムでは、一般の生徒がログインすると本の貸し出しや返却に関する機能は閲覧できますが、書籍情報の追加や削除などの機能は閲覧・利用することができません。ただし、図書館員アカウントでログインすると、書籍情報の追加や削除などの機能が表示され、利用できるはずです。
要約すると、異なるユーザーは異なる機能を使用できます。これが許可制度によって達成されることです。
どのメニューやボタンを表示するかを選択するユーザーの権限をフロントエンドだけに依存することはできません。その場合、対応する関数のインターフェイスアドレスを知っている人は、フロントエンドを経由せずに、直接、該当する関数の操作を実装するリクエストを送信できるためです。
したがって、現在のユーザーが対応する権限を持っているかどうかを判断するために、バックグラウンドでユーザーの権限を判断する必要もあり、対応する操作を実行するために必要な権限を持っている必要があります。
3.1 基本的な認証プロセス
SpringSecurity では、デフォルトの FilterSecurityInterceptor が権限の検証に使用されます。 FilterSecurityInterceptorではSecurityContextHolderからAuthenticationを取得し、続いて権限情報を取得します。現在のユーザーが現在のリソースにアクセスするために必要な権限を持っているかどうか。
したがって、プロジェクトでは、現在ログインしているユーザーの権限情報を Authentication に保存するだけで済みます。
次に、リソースに必要な権限を設定します。
3.2 認可の実装
3.2.1 リソースへのアクセスに必要な権限を制限する
SpringSecurity は、アノテーションベースの権限制御ソリューションを提供します。これは、私たちのプロジェクトで使用される主な方法でもあります。注釈を使用して、対応するリソースへのアクセスに必要な権限を指定できます。
@EnableGlobalMethodSecurity(prePostEnabled=true) //这里是添加到Securityconfig配置类当中
対応する注釈を使用できるようになります。 @PreAuthorize
@RestController
public class Hellotest {
@RequestMapping("/hello")
@PreAuthorize("hasAuthority('test')")
public String hello(){
return "hello";
}
}
3.2.2 許可情報のカプセル化
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private SysUser sysUser;
//存储权限信息
private List<String> permissions;
public LoginUser(SysUser sysUser,List<String> permissions) {
this.sysUser = sysUser;
this.permissions = permissions;
}
//存储SpringSecurity所需要的权限信息的集合
@JSONField(serialize = false)
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}
//把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
authorities = permissions.stream().
map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
3.2.3 データベースからの権限情報のクエリ
3.2.3.1 RBAC 許可モデル
RBAC 権限情報 (Role-Based Access Control) は、ロールベースの権限制御です。これは現在、開発者にとって最もユーザーフレンドリーでユニバーサルな開発モデルです。
-- 左连接where条件只影响右边的表,反之
SELECT DISTINCT sm.`perms` from sys_user_role sur
LEFT JOIN sys_role sr on sur.role_id=sr.id
LEFT JOIN sys_role_menu srm on sur.role_id=srm.role_id
LEFT JOIN sys_menu sm on sm.id=srm.menu_id
WHERE
user_id=2
AND
sr.`status`=0
AND
sm.`status`=0