Skip to content

nuiiap/spring-security-lab

Repository files navigation

Overview

Overview image

Prerequisite:

  • project ใน build.gradle เพิ่ม implement ของ spring security เข้าไป
...
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-security'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testImplementation 'org.springframework.security:spring-security-test'
        testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }
...

Lab1 Spring security with user password

Initialize project จะเจอหน้า login สำหรับทุก endpoint เลย

initial image

  • ตอน run จะมี log default password และตัว default user คือ user
  • ถ้า login ถูกจะปล่อยให้เข้าไปแล้วเจอ whitelabel
  • ถ้า login ผิดจะขึ้น Bad credentials!

เริ่มต้นด้วยการ ลองเปลี่ยน password เข้าไปใน in memory database

@EnableWebSecurity
@Configuration
public class SecurityConfig {
    @Bean
    InMemoryUserDetailsManager inMemoryUserDetailsManager() {
        String customPassword = "{noop}halow";
        return new InMemoryUserDetailsManager(
                User.withUsername("user")
                        .password(customPassword)
                        .roles("user")
                        .build()
        );
    }
}
  • จำเป็นต้องใส่ {noop} ไว้หน้า password ด้วยเพราะว่าใช้ password ตรง ๆ ไม่ได้ encrypt

ลองเพิ่ม user เข้าไปอีกสัก 10 คนนึง ด้วยการปรับ code เพื่อสร้าง collection

@EnableWebSecurity
@Configuration
public class SecurityConfig {
    @Bean
    InMemoryUserDetailsManager inMemoryUserDetailsManager() {
        String customPassword = "{noop}halow";
        List<UserDetails> users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            users.add(User.withUsername("user" + i)
                    .password(customPassword + i)
                    .roles("user")
                    .build());
        }

        return new InMemoryUserDetailsManager(users);
    }
}

ลองเปิด log trace เพื่อดูว่าจริง ๆ แล้วโดย default มันวิ่งไปที่ filter หรือ provider ตัวไหนบ้าง

logging.level.org.springframework.security=TRACE

  • default filter: UsernamePasswordAuthenticationFilter
  • default provider: DaoAuthenticationProvider
    • พอเป็น Dao มันก็จะวิ่งไปหา class UserDetailService โดยจาก code ด้านบนได้มีการ inject bean inMemoryUserDetailManager เพื่อแอบใส่ user เข้าไปใน inMemory แล้วก็จะเอา user ที่เข้ามาใน request มาเทียบกับของที่อยู่ใน inMemory เอง

Lab2 custom filter

อยากลองใช้ filter chain มาเพื่อดัก request ก่อนที่จะวิ่งไปหา AuthenticationProvider

ก่อนจะเริ่มทำเรื่อง filter จะต้องทำเรื่อง SecurityConfig ก่อน ต้องมีการ Overide ค่า SecurityFilterChain ก่อน เพื่อให้สามารถ register ตัว filter ของเราเข้าไปได้

โดยจะเริ่มจาก basic ให้มี form login เหมือนเดิม

//...
public class SecurityConfig {
    //...
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(
                        authorizeHttp -> {
                            authorizeHttp.requestMatchers("/").permitAll();
                            // authorizeHttp.anyRequest().authenticated();
                        }
                )
                .formLogin(l -> l.defaultSuccessUrl("/internal"))
                .logout(l -> l.logoutSuccessUrl("/"))
                .build();
    }
    //...
}
  • จาก code ด้านบนจะมีการ comment anyRequest().authenticated(); ไว้อยู่เพื่อดู behavior ว่าถ้าเราไม่เปิดจะเป็นยังไง

ผลลัพธ์ที่ได้คือ

  • ก็คือ โดน denied หมดเลย

แต่ถ้าเรา uncomment anyRequest().authenticated() ออกก็คือถ้า login ผ่านก็จะสามารถเข้าใช้งานได้

//...
public class SecurityConfig {
    //...
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(
                        authorizeHttp -> {
                            authorizeHttp.requestMatchers("/").permitAll();
                            authorizeHttp.anyRequest().authenticated();
                        }
                )
                .formLogin(l -> l.defaultSuccessUrl("/internal"))
                .logout(l -> l.logoutSuccessUrl("/"))
                .build();
    }
    //...
}

เพื่อทดสอบการ login ให้เพิ่ม SimpleController สักตัวนึงให้สอดคล้องกับ config ด้านบน

@RestController
public class SimpleController {
    private static final Logger logger = LoggerFactory.getLogger(SimpleController.class);
    @GetMapping("/")
    public String publicPage() {
        logger.debug("On public page");
        logger.debug("getPrincipal {}", SecurityContextHolder.getContext().getAuthentication().getPrincipal());
        return "Public";
    }

    @GetMapping("/internal")
    public String internal(Authentication authentication) {
        logger.debug("Object after login");
        logger.debug("getPrincipal {}", SecurityContextHolder.getContext().getAuthentication().getPrincipal());
        logger.debug("getName {}", authentication.getName());
        return "Hallow internal";
    }
}
  • การทดสอบเบื้องต้น เข้า localhost:8085/internal จะติดต้อง login
  • หลังจาก login เสร็จจะเจอหน้าแสดง text "Hallow internal"
  • ถ้าอยู่ที่ path / ไม่จำเป็นต้อง login แต่จะต้องเห็น text "Public"

ลองสร้าง SimpleFilter และ register เข้าไป

public class SimpleFilter extends OncePerRequestFilter {
    private static final Logger logger = LoggerFactory.getLogger(SimpleFilter.class);
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        logger.debug("hola SimpleFilter");

        filterChain.doFilter(request, response);
    }
}
  • การทำงานของ Filter คือจำเป็นต้องส่งต่อ filterChain ไปด้วย และของที่อยู่บนคำสั่ง filterChain.doFilter() จะหมายถึง ขา request ส่วน หลัง doFilter จะเป็นขา response
//...
public class SecurityConfig {
    //...
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(
                        authorizeHttp -> {
                            authorizeHttp.requestMatchers("/").permitAll();
                            authorizeHttp.anyRequest().authenticated();
                        }
                )
                .formLogin(l -> l.defaultSuccessUrl("/internal"))
                .logout(l -> l.logoutSuccessUrl("/"))
                .addFilterBefore(new SimpleFilter(), AuthorizationFilter.class)
                .build();
    }
}
  • addFilterBefore เนื่องจาก อยากให้ filter ทำงานก่อนจะเริ่มการ Authentication

Lab3 custom provider

ลองสร้าง custom provider เพื่อเพิ่ม role เข้าไปใน Authentication Object

public class SimpleProvider implements AuthenticationProvider {
    private static final Logger logger = LoggerFactory.getLogger(SimpleProvider.class);
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        logger.debug("SimpleProvider yo!");
        var name = authentication.getName();
        if (Objects.equals(name, "user2")) {
            // add role to user2
            var user2 = User.withUsername("user2")
                    .password("SET_NEW_PASSWORD")
                    .roles("user", "devops")
                    .build();
            return UsernamePasswordAuthenticationToken.authenticated(
                    user2,
                    null,
                    user2.getAuthorities()
            );
        }
        return null;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

หลังจากสร้าง file provider แล้วก็ไป register ให้ spring security รู้จัก โดยเข้าไป add เพิ่มที่ Security Config

//...
public class SecurityConfig {
    //...
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(
                        authorizeHttp -> {
                            authorizeHttp.requestMatchers("/").permitAll();
                            authorizeHttp.anyRequest().authenticated();
                        }
                )
                .formLogin(l -> l.defaultSuccessUrl("/internal"))
                .logout(l -> l.logoutSuccessUrl("/"))
                .addFilterBefore(new SimpleFilter(), AuthorizationFilter.class)
                .authenticationProvider(new SimpleProvider())
                .build();
    }
    //...
}
  • code ที่เพิ่มขึ้นคือ authenticationProvider โดย register ตัว SimpleProvider เข้าไปด้วย
  • การทำงานของ code จะเปลี่ยนไปเล็กน้อย คือ ทุกครั้งที่มีการ authentication จะวิ่งไปที่ SimpleProvider ก่อน ถ้าไม่เข้า if ก็จะไปต่อที่ default provider ซึ่งก็คือ DaoAuthenticationProvider

case1: login with user1 initial image

  • จุดสังเกต คือมีการเรียกใช้ SimpleProvider และเรียก DaoAuthenticationProvider ต่อ
  • จากนั้น ค่อย redirect ไปที่ /internal

case2: login with user2 initial image

  • จุดสังเกต คือเรียกใช้ SimpleProvider แล้วเข้าไปทำงานด้านในคือ add role devops เข้าไป จากนั้นก็ redirect กลับไปหา /internal

Lab4 Change passwordEncoder function

ลองเปลี่ยนวิธีการ encoder password เพื่อเข้าใจวิธีการของ DelegatingPasswordEncoder หลัก ๆ คือ {} จะเป็นตัวบอก function ที่ใช้ในการ encode password ตัวอย่างอื่นดูได้จาก reference อันนี้ https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html#authentication-password-storage-dpe

public class SecurityConfig {
    //...
    @Bean
    InMemoryUserDetailsManager inMemoryUserDetailsManager() {

        List<UserDetails> users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Pbkdf2PasswordEncoder encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
            String result = encoder.encode("myPassword");
            result = "{pbkdf2@SpringSecurity_v5_8}" + result;
            users.add(User.withUsername("user" + i)
                    .password(result)
                    .roles("user")
                    .build());
            System.out.println("user " + i + " password is " + users.get(i).getPassword());
        }

        return new InMemoryUserDetailsManager(users);
    }
    //...
}
  • หลังจากปรับวิธีการ endcode แล้ว login ที่หน้าเว็บอีกที user: user1, password: myPassword
  • ต้องเข้าได้ปกติ เพราะว่าการ endcode จะมีผลกับ ของที่เก็บใน database และ วิธีการ compare

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages