Google Code Prettify

顯示具有 spring security 標籤的文章。 顯示所有文章
顯示具有 spring security 標籤的文章。 顯示所有文章

2022年7月6日 星期三

jasypt 加解密

 在 application.yml 中如果有資料庫連線設定,可能會如下:

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/AUTH
    username: root
    password: ENC(wwCYqJ+PMxrCAY0aPs8v91/oykkFVyux)
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    database-platform: org.hibernate.dialect.MySQLDialect
上面密碼的部份是用 jasypt 加密,通常不會在設定檔直接寫明碼。那麼,怎麼初始化 jasypt 函式庫呢? 
  1. build.gradle
  2. implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4'  
    
    加入如上 jar 檔。
  3. application.yml
  4. jasypt:
      encryptor:
        password: ${JASYPT_ENCRYPTOR_PASSWORD}
        algorithm: PBEWithMD5AndDES
        iv-generator-classname: org.jasypt.iv.NoIvGenerator
    
    ${JASYPT_ENCRYPTOR_PASSWORD} 會從環境變數取得加解密的 key,要注意最後一行,在 jasypt-spring-boot-starter 2.x 版不需要這個設定,到 3.x 版才需要。
  5. @EnableEncryptableProperties
  6. @EnableEncryptableProperties
    @SpringBootApplication
    public class DemoSecurityApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(DemoSecurityApplication.class, args);
    	}
    }
    
    在 @SpringBootApplication 或 @Configuration 所在的 class 上加上 @EnableEncryptableProperties 啟用 jasypt。

2022年4月19日 星期二

Spring Security: AES 加解密

AES (Advanced Encryption Standard) 加密是美國聯邦政府採用的對稱式加密標準,用來取代原先使用多年的 DES (Data Encryption Standard) 加密。Spring Security 提供一個命名為 Encryptors 的類別,可以進行 AES 加解密,說明如下。

  • KeyGenerators

AES 加密需要 key 也需要 salt,這個工廠類別是密鑰產生器,產生出來的字串要當 key 或 salt 都可以,一般來說,key 會是人類取名,在必要時要求使用者需入,salt 則是系統產生,這個類別通常用來產生 salt。

如上圖,可以知道這個類別可以產生 BytesKeyGenerator 和 StringKeyGenerator 兩種密鑰產生器,BytesKeyGenerator 會產生 byte[] 的密鑰,StringKeyGenerator 則是 byte[] 的 hex 字串。

secureRandom 不帶參數時,產生的密鑰長度是 8 bytes,要指定長度就帶參數。

程式寫法如下:
String salt = KeyGenerators.string().generateKey();
  • Encryptors
產生加密類別的工廠類別,宣告如下:

這個工廠類別會產生兩種加密類別 -- BytesEncryptorTextEncryptor,這兩個類別都只有兩個 method - encrypt、descrypt,BytesEncryptor 的 method 傳入 byte[] 傳回 byte[],TextEncryptor 傳入 hex 值的字串,傳回 hex 值的字串。
@ExtendWith(SpringExtension.class)
@SpringBootTest
@Slf4j
class EncryptorsTest {
	private String salt = StringUtil.EMPTY;
	private String password = StringUtil.EMPTY;

	@BeforeEach
	void setUp() throws Exception {
		salt = KeyGenerators.string().generateKey();
		
		log.info("salt = " + salt);
		
		password = "P@ssw0rd";
	}

	@AfterEach
	void tearDown() throws Exception {
	}

	@Test
	void testBytesEncryptor() {
		
		BytesEncryptor encryptor = Encryptors.standard(password, salt);
		
		byte[] text = "習近平快GG了!".getBytes();
		
		byte[] encrypted = encryptor.encrypt(text);
		log.info(NumberUtil.toHex(encrypted));
		
		byte[] decrypted = encryptor.decrypt(encrypted);
		log.info(new String(decrypted));
	}
	
	@Test
	void testTextEncryptor() {
		TextEncryptor encryptor = Encryptors.text(password, salt);
		String encrypted = encryptor.encrypt("習近平快GG了!");
		log.info(encrypted);
		
		String decrypted = encryptor.decrypt(encrypted);
		log.info(decrypted);
	}
	
	@Test
	void testTextEncryptorAndBytesEncryptor() throws Exception {
		TextEncryptor textEncryptor = Encryptors.text(password, salt);
		String encrypted = textEncryptor.encrypt("習近平快GG了!");
		log.info(encrypted);
		
		BytesEncryptor bytesEncryptor = Encryptors.standard(password, salt);
		byte[] b = NumberUtil.hexToByte(encrypted);
		
		byte[] decryptedBytes = bytesEncryptor.decrypt(b);
		log.info(new String(decryptedBytes));
	}
}
如上,在 setUp()中產生 salt 及設定 password。testBytesEncryptor() 是測試 BytesEncryptor 的加解密,testTextEncryptor() 是測試 TextEncryptor 的加解密,testTextEncryptorAndBytesEncryptor() 則是驗證 TextEncryptor 產生的加密字串確實只是 hex 表示的字串。

2019年1月24日 星期四

Spring Security: AuthenticationManager

前一篇 (Spring Security: 資料庫認證、授權) 說明了怎麼使用資料庫認證,但是,看完可能會有點混淆,似乎 Spring Security 的認證、授權,就一定要使用到 WebSecurityConfigurerAdapter,Spring Security 一定要在 Web 中使用? 當然不是! 這裡以官方文件 Spring Security Reference 中的範例來說明。
  • AuthenticationExample
public class AuthenticationExample {

  private static AuthenticationManager am = new SampleAuthenticationManager();

  public static void main(String[] args) throws Exception {
    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    while (true) {
      System.out.println("Please enter your username:");
      String name = in.readLine();
      System.out.println("Please enter your password:");
      String password = in.readLine();
      try {
        Authentication request = new UsernamePasswordAuthenticationToken(name, password);
        Authentication result = am.authenticate(request);
        SecurityContextHolder.getContext().setAuthentication(result);
        break;
      } catch(AuthenticationException e) {
        System.out.println("Authentication failed: " + e.getMessage());
      }
    }
  
    System.out.println("Successfully authenticated. Security context contains: " +
                        SecurityContextHolder.getContext().getAuthentication());
  }
}
  • SampleAuthenticationManager
public class SampleAuthenticationManager implements AuthenticationManager {
  static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();

  static {
    AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
  }

  public Authentication authenticate(Authentication auth)
      throws AuthenticationException {
    if (auth.getName().equals(auth.getCredentials())) {
      return new UsernamePasswordAuthenticationToken(
                 auth.getName(), auth.getCredentials(), AUTHORITIES
             );
    }

    throw new BadCredentialsException("Bad Credentials");
  }
}
  • 執行結果
Please enter your username:
bob
Please enter your password:
password
Authentication failed: Bad Credentials
Please enter your username:
bob
Please enter your password:
bob
Successfully authenticated. Security context contains: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: Principal: bob; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER
程式簡單的要使用者從命令列輸入帳號、密碼,然後檢查帳密是否合法,這裡把帳密當成同一字串,所以第二次輸入 bob / bob 時,認證才成功,現在來說明一下程式。 SampleAuthenticationManager 這個類別實作 AuthenticationManager 介面,這個介面很簡單,只有一個 method - authenticate(Authentication authentication),只需要傳入一個實作 Authentication 介面的類別物件就行,範例傳入的是 UsernamePasswordAuthenticationToken 類別的物件。 UsernamePasswordAuthenticationToken 這個類別非常簡單的,它實作 Authentication,用來放使用者的帳號、密碼 (如果必要還可以放角色,範例放入了預設的角色 USER。),然後在 authenticate method 裡,就可以看到,我們取出帳號、密碼來比對,相同就回傳 token,失敗拋出 exception。 回到主程式,可以看到取得 authenticate 的回傳值後,範例程式將它放入 SecurityContextHolder 的 context 的 authentication 裡,為什麼?
Authentication result = am.authenticate(request);
SecurityContextHolder.getContext().setAuthentication(result);
這是 spring security 的要求,在認證成功後,要將使用者的 token 放入 SecurityContext 裡,未來 spring security 需要使用者的權限時,會自行取用。

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 所在類別開始的。