x213212
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
Invitee
Publish Note

Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

Your note will be visible on your profile and discoverable by anyone.
Your note is now live.
This note is visible on your profile and discoverable online.
Everyone on the web can find and read all notes of this public team.
See published notes
Unpublish note
Please check the box to agree to the Community Guidelines.
View profile
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Suggest edit
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
# Spring Security 上次說要去重寫我們的 認證中心 * jwt 結合 rsa * 改為 redis 集群 # 驗證流程 也就是說我們的認證中心 以前的話會變成走這樣子的流程 那麼的話就可以發現其實我們的授權中心壓力有點大 網路上找到這幾種流程圖 # 普通 jwt ![](https://i.imgur.com/pKWlm3E.png) 1、用戶請求登錄 2、Zuul將請求轉發到授權中心,請求授權 3、授權中心校驗完成,頒發JWT憑證 4、客戶端請求其它功能,攜帶JWT 5、Zuul將JWT交給授權中心校驗,通過後放行 6、用戶請求到達微服務 7、微服務將JWT交給鑑權中心,鑑權同時解析用戶信息 8、鑑權中心返回用戶數據給微服務 9、微服務處理請求,返迴響應 # 結合 rsa ![](https://i.imgur.com/oi24l2Y.png) 1. 我們首先利用RSA生成公鑰和私鑰。私鑰保存在授權中心,公鑰保存在Zuul和各個微服務 1. 用戶請求登錄 1. 授權中心校驗,通過後用私鑰對JWT進行簽名加密 1. 返回JWT給用戶 1. 用戶攜帶JWT訪問 1. Zuul直接通過公鑰解密JWT,進行驗證,驗證通過則放行 1. 請求到達微服務,微服務直接用公鑰解析JWT,獲取用戶信息,無需訪問授權中心 可能只會有共同一組密鑰問題,不過這樣就不能進行分開鑑權了。 ![](https://i.imgur.com/9hy3QD8.png) 那我們只能這樣動刀了 直接把 ehcache 換成 redis 那麼授權中心就負責頒發 jwt ,在zuul網關訪問服務的時候我們可以把緩存在 redis 的 password 和 username 拿來查詢 假設查詢到代表在頒發jwt的時候 將會進行加密,用自己的密碼去加密,解密的時候將會解碼我們的jwt token 去解碼中間payloadJson 取得 我們的 username 再來就是拿我們的 username 再去查詢我們的 redis 看有沒有緩存,有的話代表已經登入過 那麼我們就可以直接去用我們的 username > if (! JwtUtil.verify(token, username, userDetails.getPassword())) > > 拿我們傳過來的 token 配合 剛剛查詢的 username 在去從我們緩存在 userDetails 裡面的 password 再去重簽一次 ![](https://i.imgur.com/dktBMSA.png) 也就是說你偽造jwt也沒用,假設你沒登入過,你就不會緩存在 redis,你的token 又是透過你的 password 去簽的 所以 偽造jwt 要先猜對你的 password 然後又要在你猜對帳號然後又已經登入帳號的時候 也就是 redis seession時間存活時,這樣才能剛好進去。 # SecurityConfiguration 新增 bean * 新增 RedisTemplate * 重寫我們的 SpringCacheBasedUserCache 為 springzzz * 重寫 UserDetails 為 UserDetails * 傳入 RedisTemplate 去調用我們的 redis ```java // 开启 Security @EnableWebSecurity // 开启注解配置支持 @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userDetailsServiceImpl; // Spring Boot 的 CacheManager,这里我们使用 JCache @Autowired private CacheManager cacheManager; @Bean //指定我們的 redistemplate key 為 string value 為 CustomUserDetails public RedisTemplate<String, CustomUserDetails> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, CustomUserDetails> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 使用Jackson2JsonRedisSerialize 替换默认序列化 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); // objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); /// // // // // // objectMapper.registerModule(new SimpleModule().addDeserializer( SimpleGrantedAuthority.class, new SimpleGrantedAuthorityDeserializer())); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); // 设置value的序列化规则和 key的序列化规则 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } @Autowired private RedisTemplate<String, CustomUserDetails> redisTemplate; @Override protected void configure(HttpSecurity http) throws Exception { // 开启跨域 http.cors() .and() // security 默认 csrf 是开启的,我们使用了 token ,这个也没有什么必要了 .csrf().disable() .authorizeRequests() // 默认所有请求通过,但是我们要在需要权限的方法加上安全注解,这样比写死配置灵活很多 .anyRequest().permitAll() .and() // 添加自己编写的两个过滤器 .addFilter(new JwtAuthenticationFilter(authenticationManager())) .addFilter(new JwtAuthorizationFilter(authenticationManager(), cachingUserDetailsService(userDetailsServiceImpl))) // 前后端分离是 STATELESS,故 session 使用该策略 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } // 此处配置 AuthenticationManager,并且实现缓存 //在緩存 usercache 讀入我們的 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 对自己编写的 UserDetailsServiceImpl 进一步包装,实现缓存 CachingUserDetailsService cachingUserDetailsService = cachingUserDetailsService(userDetailsServiceImpl); // jwt-cache 我们在 ehcache.xml 配置文件中有声明 UserCache userCache = new springzz (cacheManager.getCache("jwt-cache"), redisTemplate); cachingUserDetailsService.setUserCache(userCache); System.out.println("test"); /* security 默认鉴权完成后会把密码抹除,但是这里我们使用用户的密码来作为 JWT 的生成密钥, 如果被抹除了,在对 JWT 进行签名的时候就拿不到用户密码了,故此处关闭了自动抹除密码。 */ auth.eraseCredentials(false); auth.userDetailsService(cachingUserDetailsService); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /* 此处我们实现缓存的时候,我们使用了官方现成的 CachingUserDetailsService ,但是这个类的构造方法不是 public 的, 我们不能够正常实例化,所以在这里进行曲线救国。 */ private CachingUserDetailsService cachingUserDetailsService(UserDetailsServiceImpl delegate) { Constructor<CachingUserDetailsService> ctor = null; try { ctor = CachingUserDetailsService.class.getDeclaredConstructor(UserDetailsService.class); } catch (NoSuchMethodException e) { e.printStackTrace(); } Assert.notNull(ctor, "CachingUserDetailsService constructor is null"); ctor.setAccessible(true); return BeanUtils.instantiateClass(ctor, delegate); } } ``` # SimpleGrantedAuthorityDeserializer 重寫 反向序列SimpleGrantedAuthorityDeserializer ```java class SimpleGrantedAuthorityDeserializer extends StdDeserializer<SimpleGrantedAuthority> { public SimpleGrantedAuthorityDeserializer() { super(SimpleGrantedAuthority.class); } @Override public SimpleGrantedAuthority deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { JsonNode tree = p.getCodec().readTree(p); return new SimpleGrantedAuthority(tree.get("authority").textValue()); } } ``` # springzzzz 使用構造好的 redistemplate redisTemplate.opsForValue().set(customUserDetails.getUsername(),customUserDetails); ```java /* * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.inlighting.security.security; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.Cache; import org.springframework.context.annotation.Bean; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.GenericToStringSerializer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserCache; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.cache.SpringCacheBasedUserCache; import org.springframework.util.Assert; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; /** * Caches {@link UserDetails} instances in a Spring defined {@link Cache}. * * @author Marten Deinum * @since 3.2 */ public class springzz implements UserCache { // ~ Static fields/initializers // ===================================================================================== public static final Log logger = LogFactory.getLog(springzz.class); // ~ Instance fields // ================================================================================================ // @Autowired // RedisTemplate<Object, Object> template ; // // @Bean // JedisConnectionFactory jedisConnectionFactory() { // return new JedisConnectionFactory(); // } // @Bean // RedisTemplate< String, Object > redisTemplate() { // final RedisTemplate< String, Object > template = new RedisTemplate< String, Object >(); // template.setConnectionFactory( jedisConnectionFactory() ); // template.setKeySerializer( new StringRedisSerializer() ); // // template.setHashValueSerializer( new GenericToStringSerializer< UserDetails >( UserDetails.class ) ); // // template.setValueSerializer( new GenericToStringSerializer< UserDetails >( UserDetails.class ) ); // // // template.setValueSerializer(new springSessionDefaultRedisSerializer()); // //template.setValueSerializer(new JsonRedisSerializer()); // // return template; // } // // @Bean // public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { // RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); // redisTemplate.setConnectionFactory(redisConnectionFactory); // // // 使用Jackson2JsonRedisSerialize 替换默认序列化 // Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); // // ObjectMapper objectMapper = new ObjectMapper(); // // objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // // jackson2JsonRedisSerializer.setObjectMapper(objectMapper); // // // 设置value的序列化规则和 key的序列化规则 // redisTemplate.setKeySerializer(new StringRedisSerializer()); // redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // redisTemplate.afterPropertiesSet(); // return redisTemplate; // } public final Cache cache; public final RedisTemplate<String, CustomUserDetails> redisTemplate; // ~ Constructors // =================================================================================================== public springzz(Cache cache, RedisTemplate<String, CustomUserDetails> redisTemplate) throws Exception { Assert.notNull(cache, "cache mandatory"); Assert.notNull(redisTemplate, "redisTemplate mandatory"); this.redisTemplate = redisTemplate; this.cache = cache; } // ~ Methods // ======================================================================================================== public UserDetails getUserFromCache(String username) { Cache.ValueWrapper element = username != null ? cache.get(username) : null; if (logger.isDebugEnabled()) { logger.debug("Cache hit: " + (element != null) + "; username: " + username); } //System.out.println("im here"+((UserDetails)cache.get(username)).getPassword().toString() ); if (element == null) { System.out.println("no here"+username); return null; } else { System.out.println("im here"); CustomUserDetails result = (CustomUserDetails) redisTemplate.opsForValue().get(username); System.out.println(result.getUsername()); return (UserDetails) result; // return result; } } public void putUserInCache(UserDetails user) { if (logger.isDebugEnabled()) { logger.debug("Cache put: " + user.getUsername()); } // System.out.println("Cache put:"+ user.getUsername()); // System.out.println("Cache put:"+ user.getPassword()); // System.out.println("Cache put:"+ user.getAuthorities()); //UserDetails user2 = user; //重寫userdetails CustomUserDetails customUserDetails = new CustomUserDetails (user.getUsername(),user.getPassword(),user.getAuthorities()); // Admin tmp = new CustomUserDetails(customUserDetails); System.out.println(customUserDetails.getUsername()); System.out.println(customUserDetails.getAuthorities()); redisTemplate.opsForValue().set(customUserDetails.getUsername(),customUserDetails); //原本opsForValue()是只能操作字符串的.现在就可以操作对象了 // customUserDetails result = (customUserDetails) template.opsForValue().get(customUserDetails.getUsername()+""); // System.out.println(result.toString()); // cache.put(user.getUsername(), user); } public void removeUserFromCache(UserDetails user) { if (logger.isDebugEnabled()) { logger.debug("Cache remove: " + user.getUsername()); } this.removeUserFromCache(user.getUsername()); } public void removeUserFromCache(String username) { cache.evict(username); } } ``` # 重寫後預設UserDetails CustomUserDetails ```java @JsonSerialize @JsonIgnoreProperties(ignoreUnknown = true) public class CustomUserDetails extends Admin implements UserDetails { public CustomUserDetails() { super(); } // // public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) { // TODO Auto-generated constructor stub super( username, password, authorities); } // // @Override // public void setAuthorityList(Collection<? extends GrantedAuthority> authorityList) { // List<SimpleGrantedAuthority> listGrantedAuth = new ArrayList<>(); // authorityList.forEach(auth -> { // listGrantedAuth.add(new SimpleGrantedAuthority(auth.toString())); // }); // super.setAuthorityList(listGrantedAuth); // } @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<SimpleGrantedAuthority> listGrantedAuth = new ArrayList<>(); super.getAuthorities().forEach(auth -> { listGrantedAuth.add(new SimpleGrantedAuthority(auth.toString())); }); return listGrantedAuth; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } @Override public String getPassword() { // TODO Auto-generated method stub return super.getPassword(); } @Override public String getUsername() { // TODO Auto-generated method stub return super.getUsername(); } } ``` admin ```java public class Admin implements Serializable { private String username; private String password; private Collection<? extends GrantedAuthority> authorities; public Admin() { super(); // TODO Auto-generated constructor stub } public Admin(String username, String password, Collection<? extends GrantedAuthority> authorities) { super(); this.username = username; this.password = password; this.authorities = authorities; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } // default constructor } ``` # 分析redis 分別存入重寫 CustomUserDetails 原本與預設UserDetails 原本與預設 # UserDetails ["org.springframework.security.core.userdetails.User",{"password":"$2a$10$AQol1A.LkxoJ5dEzS5o5E.QG9jD.hncoeCGdVaMQZaiYZ98V/JyRq","username":"jack","authorities":["java.util.Collections$UnmodifiableSet",[["org.springframework.security.core.authority.SimpleGrantedAuthority",{"authority":"ROLE_USER"}]]],"accountNonExpired":true,"accountNonLocked":true,"credentialsNonExpired":true,"enabled":true}] # CustomUserDetails ["org.inlighting.security.security.CustomUserDetails",{"username":"jack","password":"$2a$10$AQol1A.LkxoJ5dEzS5o5E.QG9jD.hncoeCGdVaMQZaiYZ98V/JyRq","authorities":["java.util.ArrayList",[["org.springframework.security.core.authority.SimpleGrantedAuthority",{"authority":"ROLE_USER"}]]],"enabled":true,"accountNonLocked":true,"accountNonExpired":true,"credentialsNonExpired":true}] 預設 UserDetails > Could not read JSON: Cannot construct instance of `org.springframework.security.core.authority.SimpleGrantedAuthority` (although at least one Creator exists): > > No Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator > 報錯 # 分別原因 1. construct 忘記加 建構元 2. org.springframework.core.codec.DecodingException: JSON decoding error: Cannot construct instance of org.springframework.security.core.authority.SimpleGrantedAuthority (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator); 我們的org.springframework.security.core.authority.SimpleGrantedAuthority 不能透過 json https://blog.csdn.net/m0_37893932/article/details/78259288 進行反向序列。 https://blog.csdn.net/weixin_34402408/article/details/92134715 https://rusyasoft.github.io/spring-security/2019/01/14/spring-security-session-redis-json/ ![](https://i.imgur.com/Iwowkzg.png) ![](https://i.imgur.com/SbE3ShF.png) ![](https://i.imgur.com/APineku.png) 到此我們的 改動 ehcache 就改為 redis 接下來就是看 RedisTemplate 如何去讀 redis集群 (應該蠻簡單? 也就是說我們把 db 的 二級緩存 ehcache 換成我們的 redis 新的問題來,我們的認證中心,是否可以換成集群 ,只要確保我們的 redis session 共享就可以了吧! 又要github騙星星。

Import from clipboard

Paste your markdown or webpage here...

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template has been removed or transferred.
Upgrade
All
  • All
  • Team
No template.

Create a template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

By clicking below, you agree to our terms of service.

Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
Wallet ( )
Connect another wallet

New to HackMD? Sign up

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Help & Tutorial

How to use Book mode

Slide Example

API Docs

Edit in VSCode

Install browser extension

Contacts

Feedback

Discord

Send us email

Resources

Releases

Pricing

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Get Full History Access

  • Edit version name
  • Delete

revision author avatar     named on  

More Less

Note content is identical to the latest version.
Compare
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully