Google Code Prettify

2018年7月24日 星期二

Redis 的七種資料型別

Redis 以 key、value 的方式儲存資料,但是,儲存的資料並非只有字串、數字這些簡單的物件。Redis 共支援七種資料型別 --- String、List、Set、Hash、Zset (sorted set)、HLL (HyperLogLog)、Geo,這裡會以指令操作的方式說明,但是,要真正的了解 Redis 怎麼使用它們,要自行參考「Redis 命令參考」多玩才行,因為我只使用到少部份的指令。
  • String
這是最基本的資料型別,通常就是 key、value,value 為一個字串,但是除了字串外,也可以是整數或浮點數。

get、set 是最基本的指令,把資料放入 Redis 或取出,這裡 get 是讀出來,不會移除,資料仍在 Redis 裡,要真的刪除要用 del,如下,一次可以刪多個,刪除後用 get 讀,如果該 key 值沒有資料,會傳回 nil。
  • List
雙向的鏈,所以可以由左或右 push 進 Redis 或 pop 出來,也可以從中間插入。
如上,從左邊依序放入 10、20、30,那麼由左到右看就會是 30 20 10,上面用 LRANGE 讀出,由 0 到 -1 表示全部讀出來,為什麼? 因為 List 的指標由左到右是 0、1、2 …,由右到左是 -1、-2、-3 …,所以由 0 到 -1 就表示全部。
除了依序 push,也可以從中間插入,LINSERT 就是提供這樣的功能,LINDEX 則是可以指定要讀那一個位置的元素。移除元素可以用 LPOP、RPOP、LREM 等命令。
  • Set
List 是有順序的,而且同一元素可以有兩個或多個,Set 是無序的,每個元素只能有一個。
如上,用 SADD 加入五個元素,用 SMEMBERS 顯示出所有元素,順序與加入時不同。
Set 間還可以做聯集,如上,mySet、urSet 用 SUNION 聯集得到相加的結果。
  • Hash
Hash (雜湊) 本來就是 key、value 結構,在 Redis 的 key、value 結構中又會怎麼表示呢?

如上所示,在指令 HMSET 後面的元素依序為 redis_key、hash_key1、hash_value1、hash_key2、hash_value2,所以 google、yahoo 為 Hash 的 key,www.google.com、www.yahoo.com 是它們的值。
這裡用 HKEYS 把所有的 key 顯示出來,也用 HVALS 把所有的 value 顯示出來。
  • ZSet
ZSet 是有序的集合,和 Set 一樣,元素不可重複,為了有順序,所以每個元素會有個權重 (score),順序就依權重排列。
用 ZRANK 可以顯示出某個元素在權重由小到大排列下,排在第幾位 (由 0 開始計算),用 ZREVRANK 則是反過來,權重由大到小排列下的排序。
  • HyperLogLog (HLL)
HLL 只有三個指令 - PFADD、PFCOUNT、PFMERGE,分別用來新增元素、計算個數、合併元素。HLL 和上面五種資料型別不同之處在於,它並非用來儲存資料,而是用在計算個數! 而且只適用在特殊場景! 當要計算的個數很大,可能是數千萬或更多,而且可容許誤差,例如網站的訪客數,那麼就可以使用 HLL。使用 HLL 有什麼好處? 好處在於,不管元素個數有多麼多,HLL 都只會固定的佔用 12KB 記憶體,最大可計算的個數為 2 的 64 次方。
如上,將 a b c d e f g 共 7 個元素加入 myHLL 中,用 PFCOUNT 計算得到 7,再把 a b c 加入 myHLL,再計算一次,仍是 7,因為 HLL 就像 SET 一樣,只會計算不重複的元素。接著將 1 2 3 4 5 共 5 個元素加入 urHLL,用 PFCOUNT 計算得 5,然後用 PFMERGE 合併 myHLL、urHLL 到 ourHLL,再用 PFCOUNT 計算 ourHLL 裡有幾個元素,得到 12。個數很小時不會有誤差,個數非常大時,會有不大於 1% 的誤差。
  • Geo
這個型別是用來記錄及計算經緯度、兩地的距離等,真的很特別。
我查了一下 Google Map,找出淡水和內湖的西堤的經緯度,分別為 (121.3750261, 25.1690807) 及 (121.5690305, 25.0792924),所以將它們加入 restaurants 這個變數,接著可以用 GEOPOS 取出,如果要算兩個餐廳的距離,就用 GEODIST,算出來的結果是 21.9413 公里。

2018年7月23日 星期一

Spring Data Redis

原文在「Introduction to Spring Data Redis」,我只是節錄並加上自己的心得,詳細仍要看原文。我的 Redis 不是安裝在本機,是安裝在另一台電腦 (CentOS 7) 上,Redis 預設是不提供遠端連線,所以要先修改 redis.conf 裡的設定,要修改的有兩個 - bindprotected-mode

bind 127.0.0.1 前加上 # 變註解就行了,也就是不限制那個 IP 都可以連進來。
protected-mode 改為 no 取消保護模式。
  • build.gradle
dependencies {
  compile('org.springframework.boot:spring-boot-starter-data-redis')
  compile('org.springframework.boot:spring-boot-starter-web')
  compileOnly('org.projectlombok:lombok')
  testCompile('org.springframework.boot:spring-boot-starter-test')
 
  compile group: 'redis.clients', name: 'jedis', version: '2.9.0'
}
紅色是這次的主題需要用到的。
  • JedisConfig.java
@Configuration
public class JedisConfig {
  @Bean
  public RedisTemplate<String, Object> redisTemplate() {
     RedisTemplate<String, Object> template = new RedisTemplate<>();
     template.setConnectionFactory(jedisConnectionFactory());
     
     return template;
  }
 
  @Bean
  public JedisConnectionFactory jedisConnectionFactory() {
     JedisConnectionFactory jedisConFactory = new JedisConnectionFactory();
     jedisConFactory.setHostName("192.168.70.233");
     jedisConFactory.setPort(6379);
     return jedisConFactory;
  }
}
定義這兩個 Bean,後續會用到的 CrudRepository 即是透過這它們連上 Redis 的。
  • Student.java
@RedisHash("Student")
@Data
public class Student implements Serializable {
   
    public enum Gender { 
        MALE, FEMALE
    }
 
    private String id;
    private String name;
    private Gender gender;
    private int grade;

    public Student() { }
    
    public Student(String id, String name, Gender gender, int grade) {
      this.id = id;
      this.name = name;
      this.gender = gender;
      this.grade = grade;
    }
}
這是後面我們要存到 Redis 上的物件,是屬於 Redis 中的 Hash 型別物件。
  • StudentRepository.java
@Repository
public interface StudentRepository extends CrudRepository<Student, String> {
 
}
熟悉 spring data 的人對 CrueRepository 一定不陌生,看來它不只能用在關聯式資料庫,也能用在 NoSQL 的資料庫。
  • DemoTest.java
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoTest {
 
  @Autowired
  private StudentRepository studentRepository;

  @Test
  public void test() {
    Student student = new Student("Eng2015001", "John Doe", Student.Gender.MALE, 1);
    studentRepository.save(student);
  
    Student retrievedStudent = studentRepository.findById("Eng2015001").get();
    System.out.println(retrievedStudent.toString());
  }
}

簡單的單元測試程式,將 id 為 Eng2015001 的 Student 物件放入 Redis,接著我們可以用「Redis Desktop Manager」這個小工具連到 Redis 後看到內容,如下。


2018年7月18日 星期三

Spring Security: 資料庫認證、授權

繼續前一篇 (Spring Security: getting started),這裡要將原本帳密寫在記憶體,改成帳密記錄在資料庫,也就是說,使用者登入後,系統應該到資料庫中取出帳密來比對認證。
  • Database
create table users(
  username varchar(50) not null primary key,
  password varchar(100) not null,
  enabled boolean not null
);
create table authorities (
  username varchar(50) not null,
  authority varchar(50) not null,
  constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);


insert into users(username,password,enabled)
 values('admin','$2a$10$hbxecwitQQ.dDT4JOFzQAulNySFwEpaFLw38jda6Td.Y/cOiRzDFu',true);
insert into authorities(username,authority) 
 values('admin','ROLE_ADMIN');
先在資料厙中建立上述兩個 table,並 insert 進資料,使用者只有一個 admin,密碼是加密過的,未加密前的密碼為 admin@123,加密的方式如下:
String encoded=new BCryptPasswordEncoder().encode("admin@123");
System.out.println(encoded);
  • build.gradle
buildscript {
   ext {
     springBootVersion = '2.0.3.RELEASE'
   }
   repositories {
       mavenCentral()
       jcenter()
       maven { url "https://repo.spring.io/libs-release" }
       maven { url "http://maven.springframework.org/milestone" }
       maven { url "http://repo.maven.apache.org/maven2" }
       maven { url "http://repo1.maven.org/maven2/" }
       maven { url "http://amateras.sourceforge.jp/mvn/" }
   }
   dependencies {
       classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
   }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'idv.steven.mysecurity'
version = '0.0.1'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
    jcenter()
    maven { url "https://repo.spring.io/libs-release" }
    maven { url "http://maven.springframework.org/milestone" }
    maven { url "http://repo.maven.apache.org/maven2" }
    maven { url "http://repo1.maven.org/maven2/" }
    maven { url "http://amateras.sourceforge.jp/mvn/" }
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-thymeleaf")
    compile('org.springframework.boot:spring-boot-starter-security')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compileOnly('org.projectlombok:lombok')
 
    testCompile("junit:junit")
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.springframework.security:spring-security-test')
 
    compile group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: '2.2.5'
}
我使用的是 MariaDB,所以要載入 MariaDB 的 JDBC driver,同時也要載入 spring jdbc 相關的 jar 檔,所以加入上面紅色的兩行。
  • application.properties
spring.datasource.url=jdbc:mariadb://localhost:3306/demo
spring.datasource.username=steven
spring.datasource.password=p@ssw0rd
在 application.properties 設定好 url、username、password,spring boot 就會幫我們建立好資料庫相關的連線。
  • WebSecurityConfig.java
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 @Autowired
 private DataSource dataSource;
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication().dataSource(dataSource)
            .usersByUsernameQuery("select username, password, enabled"
                + " from users where username=?")
            .authoritiesByUsernameQuery("select username, authority "
                + "from authorities where username=?")
            .passwordEncoder(new BCryptPasswordEncoder());
    }
}
前一篇為了方便將帳密放在記憶體,這裡改成紅色的方式,到資料庫中查詢,在繼承 WebSecurityConfigurerAdapter 的類別中,覆寫 configure(AuthenticationManagerBuilder auth) method,AuthenticationManagerBuilder 是個 builder pattern 的類別,設定好驗證帳密的規則後,當 spring security 由 spring container 取得 UserDetailsService object 時,會由 builder 產生一個新的 object。

注意一下上面的兩個 sql,第一個 select username, password, enabled from users where username = ? 用來認證,第二個 select username, authority from authorities where username = ? 則是查出使用者被授予那些角色? 以這個例子來說,查出來會是 ROLE_ADMIN,spring security 會忽略 ROLE_ 只認定使用者擁有 ADMIN 這個角色。

Spring Security: getting started

這是官網上的文章節錄,說明怎麼開始用 spring security 控制 web 的權限,一個很簡單的 hello 程式,第一頁是首頁,如下:
按了 here 後,會出現登入頁,如下:
輸入帳號密碼,為求簡單,程式裡先 hard code 為 user / password,輸入後按【Sign In】登入就出現 Hello 畫面,如下:
現在開始寫程式 ...

  • 程式結構

如上,在 eclipse 中以「Spring Starter Project」建立一個 spring boot 檔案,會得到如上的檔案結構,當然,裡面的程式是下面要開始慢慢加進去的。
  • build.gradle
buildscript {
    ext {
        springBootVersion = '2.0.3.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'idv.steven.mysecurity'
version = '0.0.1'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile("org.springframework.boot:spring-boot-starter-thymeleaf")
    compile('org.springframework.boot:spring-boot-starter-security')
    compile('org.springframework.boot:spring-boot-starter-web')
    compileOnly('org.projectlombok:lombok')
 
    testCompile("junit:junit")
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.springframework.security:spring-security-test')
}
在 build.gradle 寫入如上內容,因為這個程式除了會用到 spring security 外,會以 web 的方式寫程式,所以也引入 web 相關 jar 檔。
  • home.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example</title>
    </head>
    <body>
        <h1>Welcome!</h1>

        <p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
    </body>
</html>
  • login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>
        <div th:if="${param.error}">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}">
            You have been logged out.
        </div>
        <form th:action="@{/login}" method="post">
            <div><label> User Name : <input type="text" name="username"/> </label></div>
            <div><label> Password: <input type="password" name="password"/> </label></div>
            <div><input type="submit" value="Sign In"/></div>
        </form>
    </body>
</html>
  • hello.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
        <form th:action="@{/logout}" method="post">
            <input type="submit" value="Sign Out"/>
        </form>
    </body>
</html>
  • MvcConfig.java
package idv.steven.mysecurity;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }

}
WebMvcConifgurer 是 spring MVC 的一個類別,繼承它之後覆寫 addViewControllers method,可以注冊一些網址與網頁的對應。
  • WebSecurityConfig.java
package idv.steven.mysecurity;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        UserDetails user =
             User.withDefaultPasswordEncoder()
                .username("user")
                .password("password")
                .roles("USER")
                .build();

        return new InMemoryUserDetailsManager(user);
    }
}
要啟用 spring security,要在一個有 @Configuration 的類別前加上 @EnableWebSecurity,要改變預設的安全設定,則繼承 WebSecurityConfigurerAdapter,再覆寫相關的 method。這裡覆寫了 configure method 變更相關權限,從程式就很容易看得出來是什麼意思 (英文就寫的很清楚了),formLogin() 表示要使用 Form-Based login 的方式登入,登入頁為 /login,所有人都可以開啟這一個網頁。

spring security 在驗證帳密時,會由 spring container 中取得 UserDetailService 的 object 來驗證,上面的程式產生一個 UserDetailsService 的 Bean,把帳密放在記憶體裡,只有一個使用者,名稱為 user,密碼為 password,角色是 USER。一般的系統不會把帳密放記憶體,通常是放在 LDAP 或資料庫,下一篇會說明當放在資料庫時怎麼做。
  • MySecurityApplication.java
package idv.steven.mysecurity;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MySecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(MySecurityApplication.class, args);
    }
}
這個類別應該沒有懸念,spring boot 都是由標有 @SpringBootApplication 的 main 所在類別開始的。