Небольшая и красивая система безопасности Широ

Shiro

написать впереди

На протяжении всего жизненного цикла приложения мы будем говорить о проблемах безопасности данных приложения. Легитимность пользователей и видимость данных являются очень важными составляющими безопасности данных. Однако, с одной стороны, разные приложения имеют разные размеры и степень детализации требований к достоверности и видимости данных; с другой стороны, с текущей микросервисной и мультисервисной архитектурой, как совместно использовать сеансы и как кэшировать аутентификацию и авторизацию Копирование данных с высоким одновременным доступом - это насущная необходимость для нас решить. Появление Shiro позволяет нам быстро и легко решать вопросы безопасности данных в наших приложениях.

Shiro Instruction

Знакомство с Широ

Это официальное объяснение веб-сайта не является абстрактным, поэтому используйте официальный веб-сайт, чтобы объяснить напрямую: Apache Shiro™ — это мощная и простая в использовании платформа безопасности Java, которая может выполнять аутентификацию, авторизацию, шифрование и управление сеансами. Благодаря простому для понимания API Shiro вы можете быстро и легко защитить любое приложение (от самых маленьких мобильных приложений до крупнейших веб-приложений и корпоративных приложений).

Когда дело доходит до безопасности, большинство Java-разработчиков неотделимы от поддержки среды Spring.Естественно, они в первую очередь думают о Spring Security.Давайте сначала рассмотрим различия между ними.

Shiro Spring Security
Простой и гибкий сложный и громоздкий
Весна Неотделимый от весны
более грубая детализация более мелкая детализация

Хотя Spring Security является частью известной семьи Spring дома и за границей, после знакомства с Широ вы не захотите «жениться на богатой семье», а решите следовать импульсу «поэзии и дистанции».

Он виден как гребень и вершина сбоку, и расстояние отличается от расстояния (все же хорошо сначала понять концепцию)

Глядя на Широ издалека, чтобы увидеть силуэт

Subject

Это субъект, представляющий текущего «пользователя», пользователь не обязательно является конкретным человеком, а любые текущие вещи представляют собой интерактивные приложения субъекта, такие как веб-краулеры, роботы; это абстрактное понятие; все они привязаны к субъекту SecurityManager, все взаимодействия с Субъектом будут возложены на SecurityManager; Субъект можно считать фасадом; SecurityManager является фактическим исполнителем

SecurityManager

Security Manager, то есть все операции, связанные с безопасностью, будут взаимодействовать с SecurityManager, и он управляет всеми Субъектами, видно, что это ядро ​​Shiro, которое отвечает за взаимодействие с другими компонентами, представленными позже, если вы изучили SpringMVC. , вы можете думать об этом как о переднем контроллере DispatcherServlet

Realm

Домен, Широ получает данные безопасности (такие как пользователи, роли, разрешения) из Realm, то есть, если SecurityManager хочет проверить личность пользователя, ему необходимо получить соответствующего пользователя из Realm для сравнения, чтобы определить, является ли личность пользователя законный; он также должен получить соответствующий идентификатор пользователя от Realm. Роль/полномочия используются для проверки того, может ли пользователь работать; Realm можно рассматривать как DataSource, то есть безопасный источник данных.

Присмотритесь к Широ, чтобы узнать подробности.

Вдруг растеряетесь, когда посмотрите на картинку? Не паникуйте, я разберу его для вас, и посмотрите на следующее объяснение в сочетании с картинкой.Это не большая проблема, давайте посмотрим:

Subject

Вы видите, что принципалом может быть любой «пользователь», который может взаимодействовать с приложением.

SecurityManager

Эквивалент DispatcherServlet в SpringMVC; это сердце Shiro; все конкретные взаимодействия контролируются через SecurityManager; он управляет всеми субъектами и отвечает за аутентификацию и авторизацию, а также за управление сессиями и кешем

Authenticator

Аутентификатор отвечает за аутентификацию субъекта.Это точка расширения.Если пользователь считает, что Широ по умолчанию не годится, он может настроить реализацию, ему нужно настроить стратегию аутентификации, то есть при каких обстоятельствах прошла аутентификация пользователя .

Authrizer

Авторизатор или контроллер доступа используется для определения того, имеет ли субъект разрешение на выполнение соответствующей операции, то есть он контролирует, к каким функциям в приложении может получить доступ пользователь.

Realm

Может быть одна или несколько областей, которые могут рассматриваться как источник данных объекта безопасности, то есть использоваться для получения объектов безопасности; это может быть реализация JDBC, реализация LDAP или реализация памяти и т. д.; предоставляется пользователем; Примечание : Shiro не знает, где хранятся ваши пользователи/разрешения и в каком формате, поэтому нам обычно нужно реализовывать свой собственный Realm в приложениях.

SessionManager

Если вы написали сервлет, вы должны знать концепцию Session. Session нужен кто-то, кто будет управлять его жизненным циклом. Этот компонент SessionManager; и Shiro можно использовать не только в веб-среде, но и в обычной среде JavaSE, EJB и других средах. ; Следовательно, Широ абстрагирует собственный сеанс для управления данными, взаимодействующими между субъектом и приложением; в этом случае, например, когда мы используем его в веб-среде, это сначала веб-сервер, затем Сервер EJB; в настоящее время Если мы хотим поместить данные сеанса двух серверов в одно место, мы можем реализовать собственный распределенный сеанс (например, поместив данные на сервер Memcached).

SessionDAO

Все использовали DAO, объект доступа к данным, CRUD для сессии, например, если мы хотим сохранить сессию в БД, то мы можем реализовать свою SessionDAO и писать в БД через JDBC; например, если мы хотим поставить сессия в Memcached, мы можем добиться Own Memcached SessionDAO, кроме того, Cache можно использовать в SessionDAO для кэширования для повышения производительности;

CacheManager

Контроллер кэша для управления кэшами, такими как пользователи, роли, разрешения и т. д.; поскольку эти данные редко изменяются, они могут повысить производительность доступа после помещения в кэш.

Cryptography

Модуль шифрования, Широ улучшает некоторые общие компоненты шифрования, такие как шифр «шифрование/дешифрование».

Обратите внимание на структуру изображения выше. Мы разобьем объяснение шаг за шагом в соответствии с этим изображением. Запоминание этой картинки также поможет нам понять, как работает Широ., так что по-прежнему хорошо открывать две веб-страницы и смотреть их вместе.

Обзор сборки

Большинство мелких партнеров используют Spring Boot, Широ тоже определяет стартер по ситуации, и сделал более качественный пакет, которым нам удобнее пользоваться.Давайте взглянем на обзор выбора.

серийный номер название Версия
1 Springboot 2.0.4
2 JPA 2.0.4
3 Mysql 8.0.12
4 Redis 2.0.4
5 Lombok 1.16.22
6 Guava 26.0-jre
7 Shiro 1.4.0

Используйте Spring Boot, в основном, добавляя зависимую от стартера, автоматически разрешающую версию зависимостей, поэтому попробуйте сами, когда не будет проблем с последней версией, например, текущая версия Shiro - 1.5.0, а не общая проблема, мы пробуем их собственный в порядке

Добавить управление зависимостями Gradle

Общая структура каталогов

конфигурация application.yml

базовая конфигурация

Вы позволите мне увидеть это? Это всего лишь обзор, давайте сначала разберемся, давайте посмотрим на конкретную конфигурацию и завершим строительство шаг за шагом.

Часть bean-компонента shiroFilter указывает путь перехвата и соответствующий фильтр, «/user/login», «/user», «/user/loginout» могут быть доступны анонимно, другие пути требуют авторизованного доступа, shiro предоставляет и несколько фильтров по умолчанию, мы можете использовать эти фильтры для настройки разрешений для управления указанным URL-адресом (сначала вы можете это понять):

конфигурация аббревиатура соответствующий фильтр Функции
anon AnonymousFilter Указанный URL-адрес может быть доступен анонимно
authc FormAuthenticationFilter Для указанного URL-адреса требуется форма входа в систему. По умолчанию такие параметры, как имя пользователя, пароль, RememberMe и т. д., будут получены из запроса и предприняты попытки входа. Мы также можем использовать этот фильтр в качестве логики входа по умолчанию, но обычно мы сами пишем логику входа в контроллер.Если мы пишем ее сами, информацию, возвращаемую по ошибке, можно настроить.
authcBasic BasicHttpAuthenticationFilter Для указания URL-адреса требуется базовый вход в систему
Logout LogoutFilter Фильтр выхода, настройте указанный URL-адрес для реализации функции выхода из системы, что очень удобно.
noSessionCreation NoSessionCreationFilter Отключить создание сеанса
perms PermissionsAuthorizationFilter Требуется указанное разрешение на доступ
port PortFilter Необходимо указать порт для доступа
rest HttpMethodPermissionFilter Преобразуйте метод http-запроса в соответствующий глагол для создания строки разрешения. Это не имеет особого смысла. Мне интересно читать комментарии к исходному коду.
roles RolesAuthorizationFilter Требуется указанная роль для доступа
ssl SslFilter Требуется HTTPS-запрос для доступа
user UserFilter Для доступа требуется авторизованный пользователь или пользователь с функцией «запомнить меня».

дизайн таблицы базы данных

Для проектирования таблицы базы данных обратитесь к bean-компонентам в пакете entity, а структура таблицы автоматически создается с помощью аннотации @Entity и настроек JPA (вам необходимо кратко понять функции JPA).

Перейдём к делу~

Аутентификация

Аутентификация личности — это процесс подтверждения того, что «Ли Лэй — это Ли Лэй, а Хан Меймей — это Хан Меймей». Если оглянуться на рисунок выше, для этого используется модуль Realm. Широ предоставляет IniRealm, JdbcReaml, LDAPReam и другие методы аутентификации. но пользовательский The Realm обычно лучше всего подходит для наших бизнес-потребностей, а аутентификация обычно предназначена для проверки того, является ли вошедший в систему пользователь законным.

Создать нового пользователя Пользователь

@Data
@Entity
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    @Column(unique =true)
    private String username;

    private String password;

    private String salt;

}

Определить репозиторий

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    public User findUserByUsername(String username);

}

Напишите пользовательский контроллер:

@GetMapping("/login")
public void login(String username, String password) {
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    token.setRememberMe(true);
    Subject currentUser = SecurityUtils.getSubject();
    currentUser.login(token);
}

Пользовательский мир

Настройте Realm, в основном для переопределения метода doGetAuthenticationInfo(…)

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
    String username = token.getUsername();
    User user = userRepository.findUserByUsername(username);
    SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), getName());
    simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(user.getSalt()));
    return simpleAuthenticationInfo;
}

Мне нужно сделать объяснение для этих кодов, вы также можете быть полны сомнений:

  1. Как этот код применяется Широ?
  2. Как контроллер вызывает пользовательскую область?
  3. Какова цель переопределенного метода doGetAuthenticationInfo(…)?

Описание процесса сертификации

Доступ пользователя/user/loginpath, сгенерируйте UsernamePasswordToken, получите Subject (currentUser) через SecurityUtils.getSubject(), вызовите метод входа для проверки, давайте проследим код и посмотрим, как работает CustomRealm. Давайте вместе посмотрим на исходный код:

Здесь мы должны ненадолго остановиться, пожалуйста, вернитесь к крупному плану Широ и сравните с ним путь отслеживания исходного кода, он точно такой же.

Разрешить

Аутентификация — это проверка того, кем вы являетесь, а авторизация — это то, что вы можете делать,

Менеджер по продукту: модуль подписки может просматривать только отдел Программист: хорошо Менеджер по продукту: У начальника отдела больше полномочий, он также может видеть модуль подписки Программист: ок (черное лицо) Менеджер по продукту: Начальник отдела может не только читать, но и изменять данные Программист: Гуань Гун взял большой нож и покончил с собой …

Наш принцип как программистов: «Если вы можете это сделать, вы не будете шуметь»; дым пороха поднимается вверх, и верблюжий колокольчик (Широ) звучит в наших ушах: «Положи мясницкий нож и стань Будда на месте" Авторизация не такая уж хлопотная, все желающие могут обсудить...

Весь процесс в основном такой же, как и аутентификация личности, вы можете сравнить его

создание сущности роли

Когда дело доходит до авторизации, она, естественно, связана с ролями, поэтому мы создаем сущность Role:

@Data
@Entity
public class Role {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    @Column(unique =true)
    private String roleCode;

    private String roleName;
}

Новый репозиторий ролей

@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {

    @Query(value = "select roleId from UserRoleRel ur where ur.userId = ?1")
    List<Long> findUserRole(Long userId);

    List<Role> findByIdIn(List<Long> ids);

}

Определите сущность разрешения Разрешение

@Data
@Entity
public class Permission {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    @Column(unique =true)
    private String permCode;

    private String permName;
}

Определить репозиторий разрешений

@Repository
public interface PermissionRepository extends JpaRepository<Permission, Long> {

    @Query(value = "select permId from RolePermRel pr where pr.roleId in ?1")
    List<Long> findRolePerm(List<Long> roleIds);

    List<Permission> findByIdIn(List<Long> ids);
}

Выстраивайте отношения между пользователями и ролями

На самом деле связь можно сформулировать через аннотации JPA, для иллюстрации проблемы она пояснена в виде отдельного внешнего ключа.

@Data
@Entity
public class UserRoleRel {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    private Long userId;

    private Long roleId;


}

Установка ролей и разрешений

@Data
@Entity
public class RolePermRel {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;

    private Long permId;

    private Long roleId;

}

Написать пользовательский контроллер

@RequiresPermissions("user:list:view")
@GetMapping()
public void getAllUsers(){
    List<User> users = userRepository.findAll();
}

@RequiresPermissions("user:list:view")В аннотации указано, что пользователи с правами доступа user:list:view могут получить доступ), на официальном сайте четко указан формат определения разрешения, включая подстановочные знаки и т. д. Надеюсь, вы сами это проверите

Настройте метод CustomRealm (в основном переопределяющий doGetAuthorizationInfo):

Это то же самое, что и процесс аутентификации, за исключением того, что здесь больше пользователей, ролей и разрешений.

Описание процесса авторизации

Здесь авторизация выполняется с помощью комбинации фильтров (см. Конфигурация Широ) и аннотаций. Как и процесс аутентификации, он в конечном итоге переходит в нашу пользовательскую область CustomRealm. Точно так же Широ по умолчанию предоставляет множество аннотаций для обработки различных ситуаций авторизации.

аннотация Функции
@RequiresGuest Только туристы могут посетить
@RequiresAuthentication Для доступа требуется логин
@RequiresUser Вошедшие в систему или "запомнившие меня" пользователи могут получить доступ
@RequiresRoles Вошедший в систему пользователь должен иметь указанную роль для доступа
@RequiresPermissions Вошедшие в систему пользователи должны иметь определенные права доступа (если вы не хотите спорить с менеджером по продукту Хуашань, рекомендуется использовать эту аннотацию)

Официальный сайт авторизации дает четкие правила и случаи авторизации, пожалуйста, проверьте:Сделать RO.Apache.org/permissions…

В приведенном выше примере мы обращались к Mysql для получения информации об аутентификации и авторизации пользователя, что явно не соответствует потребностям производственной среды.

Управление сеансом сеанса

Студенты, которые занимались веб-разработкой, знают понятие сеанса. Наиболее часто используется время истечения сеанса, а данные находятся в CRUD сеанса. Также см. рисунок выше. Нам нужно обратить внимание на модули SessionManager и SessionDAO. . Стартер Shiro предоставил базовую информацию о конфигурации сеанса. Мы можем настроить его в YAML по мере необходимости (официальный сайт https://shiro.apache.org/spring-boot.html четко предоставил информацию о конфигурации сеанса).

Key Default Value Description
shiro.enabled true Включает модуль Широ Spring
shiro.web.enabled true Включает веб-модуль Широ Spring
shiro.annotations.enabled true Включает поддержку Spring для аннотаций Широ.
shiro.sessionManager.deleteInvalidSessions true Remove invalid session from session storage
shiro.sessionManager.sessionIdCookieEnabled true Enable session ID to cookie, for session tracking
shiro.sessionManager.sessionIdUrlRewritingEnabled true Enable session URL rewriting support
shiro.userNativeSessionManager false If enabled Shiro will manage the HTTP sessions instead of the container
shiro.sessionManager.cookie.name JSESSIONID Session cookie name
shiro.sessionManager.cookie.maxAge -1 Session cookie max age
shiro.sessionManager.cookie.domain null Session cookie domain
shiro.sessionManager.cookie.path null Session cookie path
shiro.sessionManager.cookie.secure false Session cookie secure flag
shiro.rememberMeManager.cookie.name rememberMe RememberMe cookie name
shiro.rememberMeManager.cookie.maxAge one year RememberMe cookie max age
shiro.rememberMeManager.cookie.domain null RememberMe cookie domain
shiro.rememberMeManager.cookie.path null RememberMe cookie path
shiro.rememberMeManager.cookie.secure false RememberMe cookie secure flag
shiro.loginUrl /login.jsp Login URL used when unauthenticated users are redirected to login page
shiro.successUrl / Default landing page after a user logs in (if alternative cannot be found in the current session)
shiro.unauthorizedUrl null Page to redirect user to if they are unauthorized (403 page)

В распределенных службах нам обычно нужно поместить информацию о сеансе в Redis для управления, чтобы справиться с высокими требованиями к параллельному доступу.В настоящее время нам нужно только переписать SessionDAO для завершения пользовательского управления сеансом.

Интеграция Redis

@Configuration
public class RedisConfig {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public RedisTemplate<String, Object> stringObjectRedisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }

}

Переопределить SessionDao

Глядя на исходный код, вы можете видеть, что вызывается метод retrieveSession по умолчанию SessionManager.Мы переписываем этот метод и помещаем сеанс в HttpRequest для дальнейшего повышения эффективности доступа к сеансу.

Добавить конфигурацию в ShiroConfig

Фактически, отображение кода было приведено в обзорном модуле, который приведен здесь отдельно для пояснения:

/**
 * 自定义RedisSessionDao用来管理Session在Redis中的CRUD
 * @return
 */
@Bean(name = "redisSessionDao")
public RedisSessionDao redisSessionDao(){
    return new RedisSessionDao();
}

/**
 * 自定义SessionManager,应用自定义SessionDao
 * @return
 */
@Bean(name = "customerSessionManager")
public CustomerWebSessionManager customerWebSessionManager(){
    CustomerWebSessionManager customerWebSessionManager = new CustomerWebSessionManager();
    customerWebSessionManager.setSessionDAO(redisSessionDao());
    return customerWebSessionManager;
}

/**
 * 定义Security manager
 * @param customRealm
 * @return
 */
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(CustomRealm customRealm) {
    DefaultWebSecurityManager  securityManager = new DefaultWebSecurityManager ();
    securityManager.setRealm(customRealm);
    securityManager.setSessionManager(customerWebSessionManager()); // 可不指定,Shiro会用默认Session manager
    securityManager.setCacheManager(redisCacheManagers());  //可不指定,Shiro会用默认CacheManager
//        securityManager.setSessionManager(defaultWebSessionManager());
    return securityManager;
}

/**
 * 定义session管理器
 * @return
 */
@Bean(name = "sessionManager")
public DefaultWebSessionManager defaultWebSessionManager(){
    DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
    defaultWebSessionManager.setSessionDAO(redisSessionDao());
    return defaultWebSessionManager;
}

Пока информация о сеансе управляется Redis, это сделано.

Управление кешем

При работе с распределенными сервисами это очень неэффективный способ доступа к содержимому разрешений базы данных с высоким одновременным доступом.Точно так же мы можем использовать Redis для решения этой проблемы и кэшировать данные авторизации в Redis.

Новый RedisCache

@Slf4j
@Component
public class RedisCache<K, V> implements Cache<K, V> {

    public static final String SHIRO_PREFIX = "shiro-cache:";

    @Resource
    private RedisTemplate<String, Object> stringObjectRedisTemplate;

    private String getKey(K key){
        if (key instanceof String){
            return (SHIRO_PREFIX + key);
        }
        return key.toString();
    }

    @Override
    public V get(K k) throws CacheException {
        log.info("read from redis...");
        V v = (V) stringObjectRedisTemplate.opsForValue().get(getKey(k));
        if (v != null){
            return v;
        }
        return null;
    }

    @Override
    public V put(K k, V v) throws CacheException {
        stringObjectRedisTemplate.opsForValue().set(getKey(k), v);
        stringObjectRedisTemplate.expire(getKey(k), 100, TimeUnit.SECONDS);
        return v;
    }

    @Override
    public V remove(K k) throws CacheException {
        V v = (V) stringObjectRedisTemplate.opsForValue().get(getKey(k));
        stringObjectRedisTemplate.delete((String) get(k));
        if (v != null){
            return v;
        }
        return null;
    }

    @Override
    public void clear() throws CacheException {
        //不要重写,如果只保存shiro数据无所谓
    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public Set<K> keys() {
        return null;
    }

    @Override
    public Collection<V> values() {
        return null;
    }
}

Новый RedisCacheManager

public class RedisCacheManager implements CacheManager {

    @Resource
    private RedisCache redisCache;

    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return redisCache;
    }
}

Пока что вместо того, чтобы каждый раз обращаться к БД Mysql для получения информации об аутентификации и авторизации, мы кэшируем эту информацию через Redis, что значительно повышает эффективность и отвечает требованиям дизайна распределенных систем.

Суммировать

Ответьте на официальный аккаунт «demo», чтобы получить демо-код. Это просто для того, чтобы разобраться в процессе интеграции Широ со Springboot и применении Redis для максимального использования Широ.Есть еще много подробностей об использовании Широ, и официальный сайт также очень понятен.Понимание Широ с помощью Приведенная выше диаграмма архитектуры позволит получить вдвое больший результат с половиной усилий Я чувствую, что код внутри довольно большой Насколько он большой? Это потому, что вы сами не пробовали. В сочетании с официальным сайтом и демо, я думаю, вы лучше поймете Широ. Кроме того, вы можете понять, что Широ - это мини-версия Spring Security. При авторизации он будет также очень поможет понять Spring Security.Нажмите «Читать исходный текст» в конце статьи, эффект будет лучше

Luoxia и Lonely Flying Together, осенние воды такие же, как небо, а продакт-менеджеры и программисты в гармонии...

вопрос души

  1. Говорят, что Redis однопоточный, но он очень быстрый, знаете почему?
  2. Как вы контролируете авторизацию сертификации в своем проекте? Когда авторизация меняется, это модификация?

инструменты повышения производительности

Конструктор форм MarkDown

Многие таблицы в этой статье вклеены с официального сайта, как их сразу конвертировать в MD таблицы? Такwoohoo.tables генератор.com/markdown_he…Это может помочь вам, будь то создание таблицы MD или вставка контента для создания таблицы, и контент, конечно, отличный, не только таблица MD, найдите ее сами, дополнительные инструменты, официальный ответ учетной записи «инструменты», чтобы получить


Рекомендуемое чтение


Добро пожаловать, чтобы продолжать обращать внимание на общественный номер: «Сун Гун И Бин».

  • Передовая технология Java для обмена галантереей
  • Резюме эффективных инструментов Ответ на «Инструменты»
  • Анализ вопроса интервью и ответ
  • Сбор технических данных Ответ "Данные"

Узнайте о стеке технологий Java легко и весело, думая о чтении детективных романов, и постепенно разлагайте технические проблемы, основываясь на принципах упрощения сложных проблем, конкретизации абстрактных проблем и графики.Технология постоянно обновляется, пожалуйста, продолжайте платить внимание...