написать впереди
На протяжении всего жизненного цикла приложения мы будем говорить о проблемах безопасности данных приложения. Легитимность пользователей и видимость данных являются очень важными составляющими безопасности данных. Однако, с одной стороны, разные приложения имеют разные размеры и степень детализации требований к достоверности и видимости данных; с другой стороны, с текущей микросервисной и мультисервисной архитектурой, как совместно использовать сеансы и как кэшировать аутентификацию и авторизацию Копирование данных с высоким одновременным доступом - это насущная необходимость для нас решить. Появление 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;
}
Мне нужно сделать объяснение для этих кодов, вы также можете быть полны сомнений:
- Как этот код применяется Широ?
- Как контроллер вызывает пользовательскую область?
- Какова цель переопределенного метода doGetAuthenticationInfo(…)?
Описание процесса сертификации
Доступ пользователя/user/login
path, сгенерируйте 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, осенние воды такие же, как небо, а продакт-менеджеры и программисты в гармонии...
вопрос души
- Говорят, что Redis однопоточный, но он очень быстрый, знаете почему?
- Как вы контролируете авторизацию сертификации в своем проекте? Когда авторизация меняется, это модификация?
инструменты повышения производительности
Конструктор форм MarkDown
Многие таблицы в этой статье вклеены с официального сайта, как их сразу конвертировать в MD таблицы? Такwoohoo.tables генератор.com/markdown_he…Это может помочь вам, будь то создание таблицы MD или вставка контента для создания таблицы, и контент, конечно, отличный, не только таблица MD, найдите ее сами, дополнительные инструменты, официальный ответ учетной записи «инструменты», чтобы получить
Рекомендуемое чтение
- Использовать только git pull? Иногда вы можете попробовать что-то более элегантное
- Модель родительского делегирования: часто задаваемые вопросы о крупных фабриках, которые легко решить
- Интервью не знает разницы между BeanFactory и ApplicationContext?
- Как разработать хороший RESTful API
- Почему программисты смотрят на исходный код?
Добро пожаловать, чтобы продолжать обращать внимание на общественный номер: «Сун Гун И Бин».
- Передовая технология Java для обмена галантереей
- Резюме эффективных инструментов Ответ на «Инструменты»
- Анализ вопроса интервью и ответ
- Сбор технических данных Ответ "Данные"
Узнайте о стеке технологий Java легко и весело, думая о чтении детективных романов, и постепенно разлагайте технические проблемы, основываясь на принципах упрощения сложных проблем, конкретизации абстрактных проблем и графики.Технология постоянно обновляется, пожалуйста, продолжайте платить внимание...