Spring Boot 與 gossip


你可以試著將〈套用 jdbcAuthentication〉中的 gossip 專案,遷移至 Spring Boot,這可以簡化一些設定,後續要加上一些功能時,也會方便一些。

需要的 Starter 有 AOP、JDBC、Mail、Security、Web、Thymeleaf、H2,另外,你還需要 OWASP Java Html Sanitizer、Thymeleaf 模版的 Security 方言,也就是最後 build.gradle 中會有以下設定:

...略

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-aop')
    implementation('org.springframework.boot:spring-boot-starter-jdbc')
    implementation('org.springframework.boot:spring-boot-starter-mail')
    implementation('org.springframework.boot:spring-boot-starter-security')
    implementation('org.springframework.boot:spring-boot-starter-thymeleaf')
    implementation('org.springframework.boot:spring-boot-starter-web')
    implementation('com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20171016.1')

    runtimeOnly('com.h2database:h2')
    runtimeOnly('org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.0.4.RELEASE')

    testImplementation('org.springframework.boot:spring-boot-starter-test')
    testImplementation('org.springframework.security:spring-security-test')
}

為了簡化設定,你可以將一些 .properties 的設定,直接放到 application.properties 之中:

spring.thymeleaf.cache=false

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:tcp://localhost/c:/workspace/gossip/gossip
spring.datasource.username=caterpillar
spring.datasource.password=12345678

path.url.member=/member
path.url.index=/
path.view.register_success=register_success
path.view.register_form=register
path.view.verify=verify
path.view.forgot=forgot
path.view.reset_password_form=reset_password
path.view.reset_password_success=reset_success
path.view.index=index
path.view.user=user
path.view.member=member

mail.user=yourname@gmail.com
mail.password=yourpassword

然後將專案中的靜態資源放到 static 資料夾,模版檔案放到 templates 資料夾,將 gossip.mv.db 放到專案根目錄。

原始碼的部份,將 cc.openhome.aspectcc.openhome.controllercc.openhome.model 套件中類別原始碼複製至專案。

為了簡化,暫且只使用一個 JavaConfig,也就是建立專案時標註了 @SpringBootApplication 的設定檔之中:

package cc.openhome.gossip;

...略

@SpringBootApplication(
    scanBasePackages={
        "cc.openhome.controller",
        "cc.openhome.model",
        "cc.openhome.aspect"
    }
)
public class GossipApplication {

    public static void main(String[] args) {
        SpringApplication.run(GossipApplication.class, args);
    }

    @Autowired
    private DataSource dataSource;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public WebSecurityConfigurerAdapter webSecurityConfig() {
          return new WebSecurityConfigurerAdapter() {
                @Override
                protected void configure(HttpSecurity http) throws Exception {
                    http
                        .authorizeRequests()
                        .antMatchers("/member", "/new_message", "/del_message", "/logout").hasRole("MEMBER")
                        .anyRequest().permitAll()
                        .and()
                            .formLogin().loginPage("/").loginProcessingUrl("/login")
                            .successHandler((request, response, auth) -> {
                                response.sendRedirect("/member");
                            })
                            .failureHandler((request, response, ex) -> {
                                response.sendRedirect("/?username=" + request.getParameter("username") + "&error");
                            })
                        .and()
                            .logout().logoutUrl("/logout")
                            .addLogoutHandler((request, response, auth) -> {
                                request.getSession().invalidate();
                                try {
                                    response.sendRedirect("/");
                                } catch (IOException e) {
                                    throw new UncheckedIOException(e);
                                }
                            })
                        .and()
                            .csrf().disable();
                }

                @Override
                protected void configure(AuthenticationManagerBuilder auth) throws Exception {
                    auth.jdbcAuthentication()
                        .passwordEncoder(passwordEncoder)
                        .dataSource(dataSource)
                        .usersByUsernameQuery("select name, password, enabled from t_account where name=?")
                        .authoritiesByUsernameQuery("select name, role from t_account_role where name=?");
                }
          };
    }   

    @Bean
    public PolicyFactory htmlPolicy() {
        return new HtmlPolicyBuilder()
                    .allowElements("a", "b", "i", "del", "pre", "code")
                    .allowUrlProtocols("http", "https")
                    .allowAttributes("href").onElements("a")
                    .requireRelNofollowOnLinks()
                    .toFactory();
    }
}

如此一來,就完成了專案遷移,你可以在 gossip 中找到以上的範例專案。