Rumah > Java > javaTutorial > Bagaimana untuk mengkonfigurasi Spring Security dalam pengurusan keselamatan SpringBoot

Bagaimana untuk mengkonfigurasi Spring Security dalam pengurusan keselamatan SpringBoot

WBOY
Lepaskan: 2023-05-12 21:52:10
ke hadapan
2351 orang telah melayarinya

    Rangka kerja keselamatan biasa dalam bidang pembangunan Java termasuk Shiro dan Spring Security. Shiro ialah rangka kerja pengurusan keselamatan ringan yang menyediakan pengesahan, kebenaran, pengurusan sesi, pengurusan kata laluan, pengurusan cache dan fungsi lain. Spring Security ialah rangka kerja pengurusan keselamatan yang agak kompleks dengan fungsi yang lebih berkuasa daripada Shiro, kawalan kebenaran yang lebih terperinci dan sokongan mesra untuk OAuth 2. Oleh kerana Spring Security berasal daripada keluarga Spring, ia boleh disepadukan dengan lancar dengan rangka kerja Spring , terutamanya penyelesaian konfigurasi automatik yang disediakan dalam Spring Boot, boleh menjadikan penggunaan Spring Security lebih mudah.

    Konfigurasi asas Spring Security

    Penggunaan asas

    1 Cipta projek dan tambah kebergantungan

    Buat projek Spring Boot, dan kemudian tambah spring-. boot-starter -security boleh bergantung pada

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    Salin selepas log masuk
    2 Tambah antara muka hello
    @RestController
    public class HelloController {
        @GetMapping("/hello")
        public String hello() {
            return "hello";
        }
    }
    Salin selepas log masuk
    3 Mulakan ujian projek

    Selepas berjaya memulakan, mengakses antara muka /hello akan melompat secara automatik. untuk log masuk Halaman, halaman log masuk ini disediakan oleh Spring Security

    Bagaimana untuk mengkonfigurasi Spring Security dalam pengurusan keselamatan SpringBoot

    Nama pengguna lalai ialah pengguna, dan kata laluan log masuk lalai dijana secara rawak setiap kali projek dimulakan Log permulaan projek

    Menggunakan kata laluan keselamatan yang dijana: 4f845a17-7b09-479c-8701-48000e89d364

    Selepas log masuk berjaya, pengguna boleh mengakses antara muka /hello

    Konfigurasikan nama pengguna dan kata laluan

    Jika pembangun tidak berpuas hati dengan nama pengguna dan kata laluan lalai, mereka boleh mengkonfigurasi nama pengguna lalai, kata laluan dan peranan pengguna dalam aplikasi.properties

    spring. security.user.name=tangsan
    spring.security.user.password=tangsan
    spring.security.user.roles=admin

    Pengesahan berasaskan memori

    Pembangun juga boleh menyesuaikan kelas yang diwarisi daripada WebSecurityConfigurer untuk melaksanakan lebih banyak konfigurasi tersuai bagi Spring Security, seperti pengesahan berasaskan memori Kaedah konfigurasi adalah seperti berikut:

    @Configuration
    public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Bean
        PasswordEncoder passwordEncoder() {
            return NoOpPasswordEncoder.getInstance();
        }
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("admin").password("123123").roles("ADMIN", "USER")
                    .and()
                    .withUser("tangsan").password("123123").roles("USER");
        }
    }
    Salin selepas log masuk

    Penjelasan kod:

      .
    • MyWebSecurityConfig tersuai mewarisi daripada WebSecurityConfigurerAdapter dan mengatasi kaedah konfigurasi(AuthenticationManagerBuilder auth) Dalam kaedah ini, dua pengguna dikonfigurasikan dengan dua peranan ADMIN dan USER PENGGUNA

    • Versi Spring Security yang digunakan di sini ialah 5.0.6 Kaedah penyulitan kata laluan berbilang telah diperkenalkan dalam Spring Security 5.x Pembangun mesti menetapkan satu di sini, iaitu, kata laluan tidak disulitkan

    Nota: Untuk konfigurasi pengguna berasaskan memori, tidak perlu menambah awalan "ROLE_" semasa mengkonfigurasi peranan Ini adalah sama seperti dalam Bahagian 10.2 . Terdapat perbezaan dalam pengesahan pangkalan data.

    Selepas konfigurasi selesai, mulakan semula projek dan anda boleh log masuk menggunakan dua pengguna yang dikonfigurasikan di sini.

    HttpSecurity

    Walaupun fungsi pengesahan kini boleh dilaksanakan, sumber yang dilindungi adalah lalai dan pengurusan peranan tidak boleh berdasarkan situasi sebenar Jika anda ingin melaksanakan fungsi ini, anda perlu menulis semula WebSecurityConfigurerAdapter Kaedah lain dalam

    @Configuration
    public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Bean
        PasswordEncoder passwordEncoder() {
            return NoOpPasswordEncoder.getInstance();
        }
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("root").password("123123").roles("ADMIN", "DBA")
                    .and()
                    .withUser("admin").password("123123").roles("ADMIN", "USER")
                    .and()
                    .withUser("tangsan").password("123123").roles("USER");
        }
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/admin/**")
                    .hasRole("ADMIN")
                    .antMatchers("/user/**")
                    .access("hasAnyRole(&#39;ADMIN&#39;,&#39;USER&#39;)")
                    .antMatchers("/db/**")
                    .access("hasRole(&#39;ADMIN&#39;) and hasRole(&#39;DBA&#39;)")
                    .anyRequest()
                    .authenticated()
                    .and()
                    .formLogin()
                    .loginProcessingUrl("/login")
                    .permitAll()
                    .and()
                    .csrf()
                    .disable();
        }
    }
    Salin selepas log masuk

    Penjelasan kod:

    • Pertama, tiga pengguna dikonfigurasikan Pengguna akar mempunyai peranan ADMIN dan DBA, dan pengguna pentadbir mempunyai peranan ADMIN dan USER , tangsan digunakan dengan peranan USER

    • untuk memanggil kaedah authorizeRequests() untuk membuka konfigurasi HttpSecurity, antMatchers(), hasRole(), access(. ) konfigurasi kaedah untuk mengakses laluan berbeza yang memerlukan pengguna dan peranan yang berbeza

    • anyRequest(), authenticated() menunjukkan bahawa sebagai tambahan kepada yang ditakrifkan sebelum ini, pengguna yang mengakses URL lain mesti disahkan sebelum. mengakses

    • formLogin(), loginProcessingUrl("/login"), permitAll(), bermaksud untuk mendayakan log masuk borang Halaman log masuk yang anda lihat sebelum ini turut mengkonfigurasi antara muka log masuk ke /login. Anda boleh terus memanggil antara muka /login untuk memulakan permintaan POST Untuk log masuk, nama pengguna dalam parameter log masuk mesti dinamakan nama pengguna, dan kata laluan mesti dinamakan kata laluan Antara muka loginProcessingUrl dikonfigurasikan untuk memudahkan Ajax atau terminal mudah alih panggil antara muka log masuk. Akhir sekali, permitAll dikonfigurasikan, yang bermaksud bahawa semua antara muka yang berkaitan dengan log masuk boleh diakses tanpa pengesahan.

    Selepas konfigurasi selesai, tambahkan antara muka berikut pada Pengawal untuk ujian:

    @RestController
    public class HelloController {
        @GetMapping("/admin/hello")
        public String admin() {
            return "hello admin";
        }
        @GetMapping("/user/hello")
        public String user() {
            return "hello user";
        }
        @GetMapping("/db/hello")
        public String dba() {
            return "hello dba";
        }
        @GetMapping("/hello")
        public String hello() {
            return "hello";
        }
    }
    Salin selepas log masuk

    Menurut konfigurasi di atas, akar antara muka "/admin/hello" dan pengguna pentadbir mempunyai Kebenaran akses; antara muka "/user/hello" pentadbir dan pengguna tangsan mempunyai kebenaran akses "/db/hello" hanya pengguna root yang mempunyai kebenaran akses. Menguji dalam penyemak imbas adalah mudah dan saya tidak akan menerangkan butiran di sini.

    登录表单详细配置

    目前为止,登录表单一直使用 Spring Security 提供的页面,登录成功后也是默认的页面跳转,但是,前后端分离已经成为企业级应用开发的主流,在前后端分离的开发方式中,前后端的数据交互通过 JSON 进行,这时,登录成功后就不是页面跳转了,而是一段 JSON 提示。要实现这些功能,只需要继续完善上文的配置

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin/**")
            .hasRole("ADMIN")
            .antMatchers("/user/**")
            .access("hasAnyRole(&#39;ADMIN&#39;,&#39;USER&#39;)")
            .antMatchers("/db/**")
            .access("hasRole(&#39;ADMIN&#39;) and hasRole(&#39;DBA&#39;)")
            .anyRequest()
            .authenticated()
            .and()
            .formLogin()
            .loginPage("/login_page")
            .loginProcessingUrl("/login")
            .usernameParameter("name")
            .passwordParameter("passwd")
            .successHandler(new AuthenticationSuccessHandler() {
                @Override
                public void onAuthenticationSuccess(HttpServletRequest req,
                                                    HttpServletResponse resp,
                                                    Authentication auth)
                    throws IOException {
                    Object principal = auth.getPrincipal();
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    resp.setStatus(200);
                    Map<String, Object> map = new HashMap<>();
                    map.put("status", 200);
                    map.put("msg", principal);
                    ObjectMapper om = new ObjectMapper();
                    out.write(om.writeValueAsString(map));
                    out.flush();
                    out.close();
                }
            })
            .failureHandler(new AuthenticationFailureHandler() {
                @Override
                public void onAuthenticationFailure(HttpServletRequest req,
                                                    HttpServletResponse resp,
                                                    AuthenticationException e)
                    throws IOException {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    resp.setStatus(401);
                    Map<String, Object> map = new HashMap<>();
                    map.put("status", 401);
                    if (e instanceof LockedException) {
                        map.put("msg", "账户被锁定,登录失败!");
                    } else if (e instanceof BadCredentialsException) {
                        map.put("msg", "账户名或密码输入错误,登录失败!");
                    } else if (e instanceof DisabledException) {
                        map.put("msg", "账户被禁用,登录失败!");
                    } else if (e instanceof AccountExpiredException) {
                        map.put("msg", "账户已过期,登录失败!");
                    } else if (e instanceof CredentialsExpiredException) {
                        map.put("msg", "密码已过期,登录失败!");
                    } else {
                        map.put("msg", "登录失败!");
                    }
                    ObjectMapper om = new ObjectMapper();
                    out.write(om.writeValueAsString(map));
                    out.flush();
                    out.close();
                }
            })
            .permitAll()
            .and()
            .csrf()
            .disable();
    }
    Salin selepas log masuk

    代码解释:

    • loginPage(“/login_page”) 表示如果用户未获授权就访问一个需要授权才能访问的接口,就会自动跳转到 login_page 页面让用户登录,这个 login_page 就是开发者自定义的登录页面,而不再是 Spring Security 提供的默认登录页

    • loginProcessingUrl(“/login”) 表示登录请求处理接口,无论是自定义登录页面还是移动端登录,都需要使用该接口

    • usernameParameter(“name”),passwordParameter(“passwd”) 定义了认证所需要的用户名和密码的参数,默认用户名参数是 username,密码参数是 password,可以在这里定义

    • successHandler() 方法定义了登录成功的处理逻辑。用户登录成功后可以跳转到某一个页面,也可以返回一段 JSON ,这个要看具体业务逻辑,此处假设是第二种,用户登录成功后,返回一段登录成功的 JSON 。onAuthenticationSuccess 方法的第三个参数一般用来获取当前登录用户的信息,在登录后,可以获取当前登录用户的信息一起返回给客户端

    • failureHandler 方法定义了登录失败的处理逻辑,和登录成功类似,不同的是,登录失败的回调方法里有一个 AuthenticationException 参数,通过这个异常参数可以获取登录失败的原因,进而给用户一个明确的提示

    配置完成后,使用 Postman 进行测试

    Bagaimana untuk mengkonfigurasi Spring Security dalam pengurusan keselamatan SpringBoot

    如果登录失败也会有相应的提示

    Bagaimana untuk mengkonfigurasi Spring Security dalam pengurusan keselamatan SpringBoot

    注销登录配置

    如果想要注销登录,也只需要提供简单的配置即可

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin/**")
            .hasRole("ADMIN")
            .antMatchers("/user/**")
            .access("hasAnyRole(&#39;ADMIN&#39;,&#39;USER&#39;)")
            .antMatchers("/db/**")
            .access("hasRole(&#39;ADMIN&#39;) and hasRole(&#39;DBA&#39;)")
            .anyRequest()
            .authenticated()
            .and()
            .formLogin()
            .loginPage("/login_page")
            .loginProcessingUrl("/login")
            .usernameParameter("name")
            .passwordParameter("passwd")
            .successHandler(new AuthenticationSuccessHandler() {
                @Override
                public void onAuthenticationSuccess(HttpServletRequest req,
                                                    HttpServletResponse resp,
                                                    Authentication auth)
                    throws IOException {
                    Object principal = auth.getPrincipal();
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    resp.setStatus(200);
                    Map<String, Object> map = new HashMap<>();
                    map.put("status", 200);
                    map.put("msg", principal);
                    ObjectMapper om = new ObjectMapper();
                    out.write(om.writeValueAsString(map));
                    out.flush();
                    out.close();
                }
            })
            .failureHandler(new AuthenticationFailureHandler() {
                @Override
                public void onAuthenticationFailure(HttpServletRequest req,
                                                    HttpServletResponse resp,
                                                    AuthenticationException e)
                    throws IOException {
                    resp.setContentType("application/json;charset=utf-8");
                    PrintWriter out = resp.getWriter();
                    resp.setStatus(401);
                    Map<String, Object> map = new HashMap<>();
                    map.put("status", 401);
                    if (e instanceof LockedException) {
                        map.put("msg", "账户被锁定,登录失败!");
                    } else if (e instanceof BadCredentialsException) {
                        map.put("msg", "账户名或密码输入错误,登录失败!");
                    } else if (e instanceof DisabledException) {
                        map.put("msg", "账户被禁用,登录失败!");
                    } else if (e instanceof AccountExpiredException) {
                        map.put("msg", "账户已过期,登录失败!");
                    } else if (e instanceof CredentialsExpiredException) {
                        map.put("msg", "密码已过期,登录失败!");
                    } else {
                        map.put("msg", "登录失败!");
                    }
                    ObjectMapper om = new ObjectMapper();
                    out.write(om.writeValueAsString(map));
                    out.flush();
                    out.close();
                }
            })
            .permitAll()
            .and()
            .logout()
            .logoutUrl("/logout")
            .clearAuthentication(true)
            .invalidateHttpSession(true)
            .addLogoutHandler(new LogoutHandler() {
                @Override
                public void logout(HttpServletRequest req,
                                   HttpServletResponse resp,
                                   Authentication auth) {
                }
            })
            .logoutSuccessHandler(new LogoutSuccessHandler() {
                @Override
                public void onLogoutSuccess(HttpServletRequest req,
                                            HttpServletResponse resp,
                                            Authentication auth)
                    throws IOException {
                    resp.sendRedirect("/login_page");
                }
            })
            .and()
            .csrf()
            .disable();
    }
    Salin selepas log masuk

    代码解释:

    • logout() 表示开启注销登录的配置

    • logoutUrl(“/logout”) 表示注销登录请求 URL 为 /logout ,默认也是 /logout

    • clearAuthentication(true) 表示是否清楚身份认证信息,默认为 true

    • invalidateHttpSession(true) 表示是否使 Session 失效,默认为 true

    • addLogoutHandler 方法中完成一些数据清楚工作,例如 Cookie 的清楚

    • logoutSuccessHandler 方法用于处理注销成功后的业务逻辑,例如返回一段 JSON 提示或者跳转到登录页面等

    多个 HttpSecurity

    如果业务比较复杂,也可以配置多个 HttpSecurity ,实现对 WebSecurityConfigurerAdapter 的多次扩展

    @Configuration
    public class MultiHttpSecurityConfig {
        @Bean
        PasswordEncoder passwordEncoder() {
            return NoOpPasswordEncoder.getInstance();
        }
        @Autowired
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("admin").password("123123").roles("ADMIN", "USER")
                    .and()
                    .withUser("tangsan").password("123123").roles("USER");
        }
        @Configuration
        @Order(1)
        public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter{
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.antMatcher("/admin/**").authorizeRequests()
                        .anyRequest().hasRole("ADMIN");
            }
        }
        @Configuration
        public static class OtherSecurityConfig extends WebSecurityConfigurerAdapter{
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.authorizeRequests()
                        .anyRequest().authenticated()
                        .and()
                        .formLogin()
                        .loginProcessingUrl("/login")
                        .permitAll()
                        .and()
                        .csrf()
                        .disable();
            }
        }
    }
    Salin selepas log masuk

    代码解释:

    • 配置多个 HttpSecurity 时,MultiHttpSecurityConfig 不需要继承 WebSecurityConfigurerAdapter ,在 MultiHttpSecurityConfig 中创建静态内部类继承 WebSecurityConfigurerAdapter 即可,静态内部类上添加 @Configuration 注解和 @Order注解,数字越大优先级越高,未加 @Order 注解的配置优先级最低

    • AdminSecurityConfig 类表示该类主要用来处理 “/admin/**” 模式的 URL ,其它 URL 将在 OtherSecurityConfig 类中处理

    密码加密

    1. 为什么要加密

    2. 加密方案

    Spring Security 提供了多种密码加密方案,官方推荐使用 BCryptPasswordEncoder,BCryptPasswordEncoder 使用 BCrypt 强哈希函数,开发者在使用时可以选择提供 strength 和 SecureRandom 实例。strength 越大,密码的迭代次数越多,密钥迭代次数为 2^strength 。strength 取值在 4~31 之间,默认为 10 。

    3. 实践

    只需要修改上文配置的 PasswordEncoder 这个 Bean 的实现即可

     @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(10);
    }
    Salin selepas log masuk

    参数 10 就是 strength ,即密钥的迭代次数(也可以不配置,默认为 10)。

    使用以下方式获取加密后的密码。

    public static void main(String[] args) {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(10);
        String encode = bCryptPasswordEncoder.encode("123123");
        System.out.println(encode);
    }
    Salin selepas log masuk

    修改配置的内存用户的密码

    auth.inMemoryAuthentication()
        .withUser("admin")
        .password("$2a$10$.hZESNfpLSDUnuqnbnVaF..Xb2KsAqwvzN7hN65Gd9K0VADuUbUzy")
        .roles("ADMIN", "USER")
        .and()
        .withUser("tangsan")
        .password("$2a$10$4LJ/xgqxSnBqyuRjoB8QJeqxmUeL2ynD7Q.r8uWtzOGs8oFMyLZn2")
        .roles("USER");
    Salin selepas log masuk

    虽然 admin 和 tangsan 加密后的密码不一样,但是明文都是 123123 配置完成后,使用 admin/123123,或 tangsan/123123 就可以实现登录,一般情况下,用户信息是存储在数据库中的,因此需要用户注册时对密码进行加密处理

    @Service
    public class RegService {
        public int reg(String username, String password) {
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);
            String encodePasswod = encoder.encode(password);
            return saveToDb(username, encodePasswod);
        }
        private int saveToDb(String username, String encodePasswod) {
            // 业务处理
            return 0;
        }
    }
    Salin selepas log masuk

    用户将密码从前端传来之后,通过 BCryptPasswordEncoder 实例中的 encode 方法对密码进行加密处理,加密完成后将密文存入数据库。

    方法安全

    上文介绍的认证和授权都是基于 URL 的,开发者也可通过注解来灵活配置方法安全,使用相关注解,首先要通过 @EnableGlobalMethodSecurity 注解开启基于注解的安全配置

    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
    public class MultiHttpSecurityConfig{
    }
    Salin selepas log masuk

    代码解释:

    • prePostEnabled = true 会解锁 @PreAuthorize 和 @PostAuthorize 两个注解, @PreAuthorize 注解会在方法执行前进行验证,而 @PostAuthorize 注解在方法执行后进行验证

    • securedEnabled = true 会解锁 @Secured 注解

    开启注解安全后,创建一个 MethodService 进行测试

    @Service
    public class MethodService {
        @Secured("ROLE_ADMIN")
        public String admin() {
            return "hello admin";
        }
        @PreAuthorize("hasRole(&#39;ADMIN&#39;) and hasRole(&#39;DBA&#39;)")
        public String dba() {
            return "hello dba";
        }
        @PreAuthorize("hasAnyRole(&#39;ADMIN&#39;,&#39;DBA&#39;,&#39;USER&#39;)")
        public String user() {
            return "user";
        }
    }
    Salin selepas log masuk

    代码解释:

    • @Secured(“ROLE_ADMIN”) 注解表示访问该方法需要 ADMIN 角色,注意这里需要在角色前加一个前缀 ROLE_

    • @PreAuthorize(“hasRole(‘ADMIN’) and hasRole(‘DBA’)”) 注解表示访问该方法既需要 ADMIN 角色又需要 DBA 角色

    • @PreAuthorize(“hasAnyRole(‘ADMIN’,‘DBA’,‘USER’)”) 表示访问该方法需要 ADMIN 、DBA 或 USER 角色中至少一个

    • @PostAuthorize 和 @PreAuthorize 中都可以使用基于表达式的语法

    最后在 Controller 中注入 Service 并调用 Service 中的方法进行测试

    @RestController
    public class HelloController {
        @Autowired
        MethodService methodService;
        @GetMapping("/hello")
        public String hello() {
            String user = methodService.user();
            return user;
        }
        @GetMapping("/hello2")
        public String hello2() {
            String admin = methodService.admin();
            return admin;
        }
        @GetMapping("/hello3")
        public String hello3() {
            String dba = methodService.dba();
            return dba;
        }
    }
    Salin selepas log masuk

    admin 访问 hello

    Bagaimana untuk mengkonfigurasi Spring Security dalam pengurusan keselamatan SpringBoot

    admin 访问 hello2

    Bagaimana untuk mengkonfigurasi Spring Security dalam pengurusan keselamatan SpringBoot

    admin 访问 hello3

    Bagaimana untuk mengkonfigurasi Spring Security dalam pengurusan keselamatan SpringBoot

    Atas ialah kandungan terperinci Bagaimana untuk mengkonfigurasi Spring Security dalam pengurusan keselamatan SpringBoot. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

    Label berkaitan:
    sumber:yisu.com
    Kenyataan Laman Web ini
    Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
    Tutorial Popular
    Lagi>
    Muat turun terkini
    Lagi>
    kesan web
    Kod sumber laman web
    Bahan laman web
    Templat hujung hadapan