Spring Security基于數(shù)據(jù)庫(kù)的ABAC屬性權(quán)限模型實(shí)戰(zhàn)開發(fā)教程
1. 前言
今天博主又抽空來(lái)給小伙伴更新 Spring Security 教程啦,上個(gè)章節(jié)中我們講解了如何通過(guò)數(shù)據(jù)庫(kù)實(shí)現(xiàn)基于數(shù)據(jù)庫(kù)的動(dòng)態(tài)用戶認(rèn)證,大家可能發(fā)現(xiàn)了,項(xiàng)目中是基于RBAC角色模型的權(quán)限控制,雖然能滿足大多數(shù)場(chǎng)景,但在面對(duì)復(fù)雜、細(xì)粒度的權(quán)限需求時(shí)可能會(huì)力不從心。基于屬性的訪問(wèn)控制(ABAC)模型則通過(guò)評(píng)估用戶、資源、環(huán)境等多種屬性,實(shí)現(xiàn)更加靈活的權(quán)限控制。
例如,某個(gè)菜單的訪問(wèn)可能不僅取決于用戶角色,還取決于用戶的部門、時(shí)間或其他屬性。因此,需要在權(quán)限驗(yàn)證時(shí)動(dòng)態(tài)獲取這些屬性,并進(jìn)行評(píng)估。那么本章節(jié)我們就來(lái)講解基于數(shù)據(jù)庫(kù)的ABAC屬性權(quán)限模型實(shí)戰(zhàn)開發(fā)
2. 權(quán)限決策依據(jù)
既然談到了 RBAC 和 ABAC 兩個(gè)模型,就大家介紹下兩者間的區(qū)別:
RBAC
- 核心思想:以角色作為權(quán)限管理的核心,每個(gè)用戶被賦予一個(gè)或多個(gè)角色,而角色與權(quán)限之間存在固定的映射關(guān)系。
- 決策依據(jù):當(dāng)用戶請(qǐng)求訪問(wèn)資源時(shí),系統(tǒng)根據(jù)用戶所屬角色所擁有的權(quán)限進(jìn)行校驗(yàn)。
- 粒度:粒度相對(duì)較粗,因?yàn)闄?quán)限是綁定在角色上的,無(wú)法針對(duì)單個(gè)請(qǐng)求條件進(jìn)行動(dòng)態(tài)決策。
ABAC
- 核心思想:以屬性(Attribute)為基礎(chǔ),利用用戶屬性、資源屬性、環(huán)境屬性等多個(gè)維度的條件進(jìn)行權(quán)限判斷。
- 決策依據(jù):權(quán)限決策是基于各種屬性之間的邏輯表達(dá)式和策略規(guī)則來(lái)動(dòng)態(tài)確定是否允許訪問(wèn)。
- 粒度:支持非常細(xì)粒度的控制,可以針對(duì)具體屬性制定規(guī)則,實(shí)現(xiàn)精準(zhǔn)的權(quán)限控制。
綜合對(duì)比
| 對(duì)比維度 | RBAC | ABAC |
|---|---|---|
| 決策依據(jù) | 用戶所屬角色與預(yù)定義權(quán)限映射關(guān)系 | 用戶、資源及環(huán)境屬性和策略規(guī)則 |
| 靈活性 | 固定、靜態(tài)權(quán)限模型 | 動(dòng)態(tài)、可擴(kuò)展的權(quán)限決策模型 |
| 管理難度 | 管理較簡(jiǎn)單,但角色關(guān)系復(fù)雜時(shí)易混亂 | 規(guī)則管理復(fù)雜,但擴(kuò)展靈活 |
| 粒度 | 較粗,難以細(xì)化至個(gè)性化條件 | 非常細(xì)粒度,可實(shí)現(xiàn)精確權(quán)限控制 |
| 適用場(chǎng)景 | 企業(yè)內(nèi)部、權(quán)限固定的系統(tǒng) | 復(fù)雜、多變、動(dòng)態(tài)決策的業(yè)務(wù)系統(tǒng) |
3. 數(shù)據(jù)庫(kù)表結(jié)構(gòu)說(shuō)明
上一個(gè)章節(jié)RBAC角色模型我們使用了五張表,sys_user 、 sys_role、 sys_user_role 、sys_menu 、 sys_role_menu ,需要數(shù)據(jù)表結(jié)構(gòu)的小伙伴可以查閱上一章內(nèi)容!本章節(jié)不再贅述
現(xiàn)在我們?cè)趥鹘y(tǒng)RBAC模型基礎(chǔ)上,加入ABAC屬性權(quán)限模型 ABAC(Attribute-Based Access Control)引入了更細(xì)粒度的動(dòng)態(tài)控制維度:
為什么要增加ABAC屬性權(quán)限模型?
需求1:請(qǐng)求某個(gè)方法除了要驗(yàn)證用戶角色或菜單資源,我還要判斷用戶屬性部門=IT,國(guó)家是ZH
需求2:請(qǐng)求某個(gè)方法除了要驗(yàn)證用戶角色或菜單資源,我還要限制訪問(wèn)時(shí)間段
而如果使用ABAC屬性權(quán)限模型動(dòng)態(tài)策略就可以很輕松解決這樣的問(wèn)題!
基于上述的需求,我們來(lái)擴(kuò)展我們的數(shù)據(jù)庫(kù)表,sys_user_attr 為用戶相關(guān)屬性,sys_policy 為策略表(ABAC規(guī)則存儲(chǔ))
-- 擴(kuò)展用戶屬性表(新增)
CREATE TABLE sys_user_attr (
user_id BIGINT NOT NULL,
attr_key VARCHAR(50) NOT NULL,
attr_value VARCHAR(100) NOT NULL,
PRIMARY KEY (user_id, attr_key)
);
-- 示例數(shù)據(jù)
INSERT INTO sys_user_attr VALUES
(1, 'department', 'IT'),
(2, 'department', 'HR'),
(3, 'security_level', '3');
(1, 'country', 'zh'),權(quán)限策略表設(shè)計(jì):
存儲(chǔ) ABAC 策略,每條策略包含一個(gè)條件表達(dá)式(基于 SpEL 編寫)
CREATE TABLE sys_policy (
policy_id BIGINT AUTO_INCREMENT PRIMARY KEY,
policy_name VARCHAR(50) NOT NULL,
target_resource VARCHAR(64) NOT NULL,
condition_expression VARCHAR(255) NOT NULL
);
INSERT INTO sys_policy VALUES
(1, 'IT部門訪問(wèn)策略', 'admin:menu', "#user.attrs['department'] == 'IT'"),
(2, '高安全級(jí)別策略', 'developers:menu', "T(Integer).parseInt(#user.attrs['security_level']) >= 3");
(3, 'IT部門訪問(wèn)策略', 'admin:menu', "#user.attrs['country'] == 'zh'");整體數(shù)據(jù)庫(kù)結(jié)構(gòu)如下:

4. 實(shí)戰(zhàn)開始
接下來(lái)在之前的 Maven 項(xiàng)目中,我們復(fù)用上個(gè)章節(jié)的子模塊并命名 abac-spring-security
由于涉及數(shù)據(jù)庫(kù)操作以及整合mybatis-plus,上一章節(jié)博主已經(jīng)進(jìn)行了配置的詳解這里就簡(jiǎn)單貼出代碼供大家參考:
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!--使用 HikariCP 連接池-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- mysql驅(qū)動(dòng) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.9</version>
</dependency>配置yml文件,運(yùn)行項(xiàng)目確保項(xiàng)目能正常鏈接數(shù)據(jù)庫(kù)且啟動(dòng)成功
server:
port: 8084
spring:
application:
name: db-spring-security #最新Spring Security實(shí)戰(zhàn)教程(六)基于數(shù)據(jù)庫(kù)的ABAC屬性權(quán)限模型實(shí)戰(zhàn)開發(fā)
datasource:
url: jdbc:mysql://localhost:3306/slave_db?useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 5
mybatis-plus:
configuration:
map-underscore-to-camel-case: true # 開啟駝峰轉(zhuǎn)換
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL
cache-enabled: true # 開啟二級(jí)緩存
global-config:
db-config:
logic-delete-field: delFlag # 邏輯刪除字段
logic-delete-value: 1 # 刪除值
logic-not-delete-value: 0 # 未刪除值5. MyBatis-Plus實(shí)體定義
接下來(lái)我們開始編寫業(yè)務(wù)代碼
? 用戶實(shí)體(實(shí)現(xiàn)UserDetails)
@Data
@TableName("sys_user")
public class SysUser implements UserDetails {
@TableId(type = IdType.AUTO)
private Long userId;
@TableField("login_name")
private String username; // Spring Security認(rèn)證使用的字段
private String password;
private String status; // 狀態(tài)(0正常 1鎖定)
private String delFlag; // 刪除標(biāo)志(0代表存在 1代表刪除)
@TableField(exist = false)
private List<SysRole> roles;
@TableField(exist = false)
private Map<String, String> attrs; // 用戶的屬性集合,用于 ABAC 動(dòng)態(tài)權(quán)限評(píng)估
// 實(shí)現(xiàn)UserDetails接口
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 組裝 GrantedAuthority 集合,將角色和菜單權(quán)限都加入
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.addAll(roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getRoleKey()))
.collect(Collectors.toList()));
authorities.addAll(roles.stream()
.flatMap(role -> role.getMenus().stream())
.map(menu -> new SimpleGrantedAuthority(menu.getPerms()))
.collect(Collectors.toList()));
return authorities;
}
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() {
return "0".equals(status);
}
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() {
return "0".equals(delFlag);
}
}? 角色實(shí)體
@Data
@TableName("sys_role")
public class SysRole {
@TableId(type = IdType.AUTO)
private Long roleId;
private String roleName;
private String roleKey;
@TableField(exist = false)
private List<SysMenu> menus;
}? 菜單實(shí)體
@Data
@TableName("sys_menu")
public class SysMenu {
@TableId(type = IdType.AUTO)
private Long menuId;
private String menuName;
private String perms;
}? 用戶屬性實(shí)體
@Data
public class SysUserAttr {
private Long userId;
private String attrKey;
private String attrValue;
}? 決策表實(shí)體
@Data
@TableName("sys_policy")
public class SysPolicy {
@TableId(type = IdType.AUTO)
private Long policyId;
private String policyName;
private String targetResource;
private String conditionExpression;
}6. MyBatis-Plus Mapper配置
除了 UserMapper 增加 selectUserAttrByUserId 方法以及新增 SysPolicyMapper,其余代碼與上個(gè)章節(jié)一致!
UserMapper接口 : 主要是通過(guò)用戶角色中間表獲取角色信息(角色信息中又包含了菜單信息)
@Mapper
public interface UserMapper extends BaseMapper<SysUser> {
@Select("SELECT r.* FROM sys_role r " +
"JOIN sys_user_role ur ON r.role_id = ur.role_id " +
"WHERE ur.user_id = #{userId}")
@Results({
@Result(property = "roleId", column = "role_id"),
@Result(property = "menus", column = "role_id",
many = @Many(select = "com.toher.springsecurity.demo.abac.mapper.MenuMapper.selectByUserId"))
})
List<SysRole> selectRolesByUserId(Long userId);
/**
* 獲取用戶屬性
* @param userId
* @return
*/
@Select("SELECT * FROM sys_user_attr WHERE user_id = #{userId}")
List<SysUserAttr> selectUserAttrByUserId(Long userId);
}RoleMapper接口 : 主要是通過(guò)角色菜單中間表獲取菜單信息
@Mapper
public interface RoleMapper extends BaseMapper<SysRole> {
@Select("SELECT m.* FROM sys_menu m " +
"JOIN sys_role_menu rm ON m.menu_id = rm.menu_id " +
"WHERE rm.role_id = #{roleId}")
List<SysMenu> selectMenusByRoleId(Long roleId);
}RoleMapper接口 : 主要是通過(guò)角色菜單中間表獲取菜單信息
@Mapper
public interface MenuMapper extends BaseMapper<SysMenu> {
@Select("SELECT DISTINCT m.* FROM sys_menu m " +
"JOIN sys_role_menu rm ON m.menu_id = rm.menu_id " +
"JOIN sys_user_role ur ON rm.role_id = ur.role_id " +
"WHERE ur.user_id = #{userId}")
List<SysMenu> selectByUserId(Long userId);
}SysPolicyMapper接口
@Mapper
public interface SysPolicyMapper extends BaseMapper<SysPolicy> {
}7. 自定義UserDetailsService實(shí)現(xiàn)
自定義 UserDetailsService 繼承 UserDetailsService,重寫 loadUserByUsername 方法,注入 UserMapper 以及 roleMapper通過(guò)用戶名查詢數(shù)據(jù)庫(kù)數(shù)據(jù),同時(shí)將用戶的角色、菜單資源集合、用戶屬性集合 一并賦值;
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
private final RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 查詢基礎(chǔ)用戶信息
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getUsername, username);
SysUser user = userMapper.selectOne(wrapper);
if (user == null) {
throw new UsernameNotFoundException("用戶不存在");
}
// 2. 加載角色和權(quán)限
List<SysRole> roles = userMapper.selectRolesByUserId(user.getUserId());
roles.forEach(role ->
role.setMenus(roleMapper.selectMenusByRoleId(role.getRoleId()))
);
user.setRoles(roles);
// 3. 檢查賬戶狀態(tài)
if (!user.isEnabled()) {
throw new DisabledException("用戶已被禁用");
}
// 4. 用戶的屬性集合,用于 ABAC 動(dòng)態(tài)權(quán)限評(píng)估
List<SysUserAttr> attrs = userMapper.selectUserAttrByUserId(user.getUserId());
// 轉(zhuǎn)成map集合
user.setAttrs(attrs.stream()
.collect(Collectors.toMap(s -> s.getAttrKey(), s -> s.getAttrValue())));
return user;
}
}8. 實(shí)現(xiàn)方式一:自定義MethodSecurityExpressionHandler
編寫策略決策引擎
@Component
public class AbacDecisionEngine {
private final SpelExpressionParser parser = new SpelExpressionParser();
@Autowired
private SysPolicyMapper sysPolicyMapper;
public boolean check(Authentication authentication, String resource) {
SysUser userDetails = (SysUser) authentication.getPrincipal();
// 加載策略集
LambdaQueryWrapper<SysPolicy> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysPolicy::getTargetResource, resource);
List<SysPolicy> policies = sysPolicyMapper.selectList(queryWrapper);
if (policies.isEmpty()) {
return false;
}
// 構(gòu)建評(píng)估上下文
EvaluationContext context = new StandardEvaluationContext();
// 將用戶傳入表達(dá)式上下文 如:#user.attrs['department'] == 'IT'
// 其中user前綴就是我們傳入的user
context.setVariable("user", userDetails);
return policies.stream().allMatch(policy ->
parser.parseExpression(policy.getConditionExpression()).getValue(context, Boolean.class)
);
}
}重寫PermissionEvaluator
@RequiredArgsConstructor
public class AbacPermissionEvaluator implements PermissionEvaluator {
private final AbacDecisionEngine abacEngine;
@Override
public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {
return abacEngine.check(auth, (String)permission);
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
// 本示例僅實(shí)現(xiàn) hasPermission(Authentication, Object, Object)
return false;
}
}9. 實(shí)現(xiàn)方式二:自定義注解
使用自定義注解,Spring Security 將在每次方法調(diào)用時(shí)調(diào)用該 bean 上給定的方法
@Component("authz")
@RequiredArgsConstructor
public class AuthorizationLogic {
private final SpelExpressionParser parser = new SpelExpressionParser();
private final SysPolicyMapper sysPolicyMapper;
public boolean check(MethodSecurityExpressionOperations operations, String permission) {
SysUser userDetails = (SysUser) operations.getAuthentication().getPrincipal();
// 加載策略集
LambdaQueryWrapper<SysPolicy> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysPolicy::getTargetResource, permission);
List<SysPolicy> policies = sysPolicyMapper.selectList(queryWrapper);
if (policies.isEmpty()) {
return false;
}
// 構(gòu)建評(píng)估上下文
EvaluationContext context = new StandardEvaluationContext();
// 將用戶傳入表達(dá)式上下文 如:#user.attrs['department'] == 'IT'
// 其中user前綴就是我們傳入的user
context.setVariable("user", userDetails);
return policies.stream().allMatch(policy ->
parser.parseExpression(policy.getConditionExpression()).getValue(context, Boolean.class)
);
}
}10. Spring Security配置文件
@Configuration
//開啟方法級(jí)的安全控制
@EnableMethodSecurity
@RequiredArgsConstructor
public class AbacSecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final AbacDecisionEngine abacEngine;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.
authorizeHttpRequests(authorize -> authorize
.requestMatchers("/setPassword").permitAll()
//配置形式ADMIN角色可以訪問(wèn)/admin/view
.requestMatchers("/admin/view").hasRole("ADMIN")
.anyRequest().authenticated())
.userDetailsService(userDetailsService)
.formLogin(withDefaults())
.logout(withDefaults())
;
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置 Method Security Expression Handler,使用自定義的 PermissionEvaluator
*/
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(AbacDecisionEngine abacEngine) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(new AbacPermissionEvaluator(abacEngine));
return handler;
}11. controller測(cè)試文件
新增一個(gè) AdminController 作為ABAC屬性權(quán)限模型的測(cè)試
@RestController
@RequestMapping("/api")
public class AdminController {
/**
* MethodSecurityExpressionHandler方式
*
* @return
*/
@PreAuthorize("hasPermission(null, 'admin:menu')")
@GetMapping("/admin")
public ResponseEntity<?> getAdminData() {
return ResponseEntity.ok("MethodSecurityExpressionHandler方式");
}
/**
* 自定義注解的方式
* @return
*/
@PreAuthorize("@authz.check(#root, 'admin:menu')")
@GetMapping("/authz")
public ResponseEntity<?> authz() {
return ResponseEntity.ok("自定義注解的方式");
}
/**
* 以下RBAC角色 + ABAC屬性的混合校驗(yàn) 可以復(fù)制測(cè)試
* @PreAuthorize("hasAuthority('admin:menu') and @abacDecisionEngine.check(authentication, 'admin:menu')")
*
* @PreAuthorize("hasRole('ADMIN') and @authz.check(#root, 'admin:menu')")
*
* @return
*/
@PreAuthorize("hasRole('ADMIN') and @abacDecisionEngine.check(authentication, 'admin:menu')")
@GetMapping("/admin/test")
public ResponseEntity<?> test() {
return ResponseEntity.ok("RBAC角色 + ABAC屬性的混合校驗(yàn)");
}
}小伙伴們可以根據(jù)博主的代碼編寫完成后,進(jìn)行運(yùn)行測(cè)試,新增用戶屬性并可以加入更多的策略來(lái)測(cè)試,如:

這里博主順便整理一些常見的策略以供大家參考:
| 場(chǎng)景描述 | SpEL表達(dá)式 |
|---|---|
| 時(shí)間段訪問(wèn)控制 | T(java.time.LocalTime).now().isBetween('09:00', '17:00') |
| 安全等級(jí)驗(yàn)證 | attrs['securityLevel'] >= 3 && authentication.isAuthenticated() |
| 地理位置限制 | attrs['country'] == 'CN' && attrs['ipRegion'] == 'Shanghai' |
| 多因素認(rèn)證驗(yàn)證 | attrs['mfaEnabled'] == true && authentication.getAuthorities().contains('MFA_VERIFIED') |
12. 完整工作流程
請(qǐng)求到達(dá):GET /api/admin
身份認(rèn)證:通過(guò) UserDetailsService 加載用戶信息
屬性加載:從sys_user_attr 表獲取用戶屬性
策略匹配:查詢sys_policy 表中 target_resource 為 admin:menu 的策略
表達(dá)式評(píng)估:使用SpEL評(píng)估 #user.attrs['department'] == 'IT'
訪問(wèn)決策:所有策略滿足即允許訪問(wèn)
14. 總結(jié)
通過(guò)本章節(jié)相信大家對(duì)ABAC屬性權(quán)限模型的開發(fā)已經(jīng)能掌握了,值得一提的是在實(shí)際開發(fā)中,我們?yōu)榱吮苊鈹?shù)據(jù)庫(kù)壓力建議還要對(duì)用戶信息、策略信息等采用緩存處理,相關(guān)用戶屬性、決策也可以按照自身需求進(jìn)行拓展!
通過(guò)RBAC角色模型 + ABAC屬性權(quán)限模型這種設(shè)計(jì),你可以靈活地根據(jù)業(yè)務(wù)變化調(diào)整權(quán)限策略,實(shí)現(xiàn)更細(xì)粒度的安全控制。希望這篇實(shí)戰(zhàn)文章能夠?yàn)槟愕捻?xiàng)目開發(fā)提供參考與啟發(fā)!
到此這篇關(guān)于Spring Security基于數(shù)據(jù)庫(kù)的ABAC屬性權(quán)限模型實(shí)戰(zhàn)開發(fā)教程的文章就介紹到這了,更多相關(guān)Spring Security ABAC權(quán)限模型內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java實(shí)現(xiàn)收藏名言語(yǔ)句臺(tái)詞的app
本文給大家分享的是使用java制作的記錄名人名言臺(tái)詞等等讓你難忘的語(yǔ)句的APP的代碼,非常的實(shí)用,有需要的小伙伴可以參考下。2015-04-04
Netty實(shí)現(xiàn)簡(jiǎn)易版的RPC框架過(guò)程詳解
這篇文章主要為大家介紹了Netty實(shí)現(xiàn)簡(jiǎn)易版的RPC框架過(guò)程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
Java8的Stream()與ParallelStream()的區(qū)別說(shuō)明
這篇文章主要介紹了Java8的Stream()與ParallelStream()的區(qū)別說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
關(guān)于Idea創(chuàng)建Java項(xiàng)目并引入lombok包的問(wèn)題(lombok.jar包免費(fèi)下載)
很多朋友遇到當(dāng)idea創(chuàng)建java項(xiàng)目時(shí),命名安裝了lombok插件卻不能使用注解,原因有兩個(gè)大家可以參考下本文,本文對(duì)每種原因分析給出了解決方案,需要的朋友參考下吧2021-06-06
Java基于socket實(shí)現(xiàn)簡(jiǎn)易聊天室實(shí)例
這篇文章主要介紹了Java基于socket實(shí)現(xiàn)簡(jiǎn)易聊天室的方法,實(shí)例分析了java基于socket實(shí)現(xiàn)聊天室服務(wù)端與客戶端的相關(guān)技巧,需要的朋友可以參考下2015-05-05
java實(shí)現(xiàn)小型局域網(wǎng)群聊功能(C/S模式)
這篇文章主要介紹了java利用TCP協(xié)議實(shí)現(xiàn)小型局域網(wǎng)群聊功能(C/S模式) ,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-08-08
Spring的自定義擴(kuò)展標(biāo)簽NamespaceHandler解析
這篇文章主要介紹了Spring的自定義擴(kuò)展標(biāo)簽NamespaceHandler解析,在很多情況下,我們需要為系統(tǒng)提供可配置化支持,簡(jiǎn)單的做法可以直接基于Spring的標(biāo)準(zhǔn)Bean來(lái)配置,Spring提供了可擴(kuò)展Schema的支持,這是一個(gè)不錯(cuò)的折中方案,需要的朋友可以參考下2023-12-12

