套用 jdbcAuthentication


如果想讓目前的 gossip 專案,可以套用〈JDBC 驗證與授權〉該怎麼做呢?資料表格的部份,勢必得做些修改了:

CREATE TABLE t_account (
  name VARCHAR(15) NOT NULL,
  email VARCHAR(128) NOT NULL,
  password VARCHAR(64) NOT NULL,
  enabled TINYINT NOT NULL,
  PRIMARY KEY (name)
);

CREATE TABLE t_account_role (
    name VARCHAR(15) NOT NULL,
    role VARCHAR(15) NOT NULL,
    PRIMARY KEY (name, role)
);

CREATE TABLE t_message (
    name VARCHAR(15) NOT NULL,
    time BIGINT NOT NULL,
    blabla VARCHAR(512) NOT NULL,
    FOREIGN KEY (name) REFERENCES t_account(name) 
);

接下來要修改 UserService,因為在這邊打算採用 Spring Security 的 PasswordEncoder 來進行密碼編碼:

package cc.openhome.model;

...略

@Service
public class UserService {
    @Autowired
    private AccountDAO accountDAO;

    @Autowired
    private MessageDAO messageDAO;

    @Autowired
    private PasswordEncoder passwordEncoder;


    ... 略

    private Account createUser(String username, String email, String password) {
        Account acct = new Account(username, email, passwordEncoder.encode(password));
        accountDAO.createAccount(acct);
        return acct;
    }

    ... 略

    public void resetPassword(String name, String password) {
        accountDAO.updatePassword(name, passwordEncoder.encode(password));
    }
}

因為不用自行產生鹽值了,建立使用者與重置密碼的部份就簡單許多,而不必要的 loginencryptedPassword 方法也可以刪掉;因為不需要鹽值了,Accountsalt 欄位可以去除,AccountDAOupdatePasswordSalt 可以修改為 updatePassword,而實作品 AccountDAOJdbcImpl 也得做出因應的修改,在 SQL 的部份,也要因應表格欄位變化而調整,這部份沒什麼困難度,細心點處理就好了。

在這邊還想要將 "login" 給處理掉,因為目前它只是用來取得登入的使用者名稱,而這部份,可以使用 Principal 來處理,這主要是在 MememberController 之中:

package cc.openhome.controller;

...略

@Controller
public class MemberController {
    @Value("${path.view.member}")
    private String MEMBER_PATH;

    @Value( "#{'redirect:' + '${path.url.member}'}")
    private String REDIRECT_MEMBER_PATH;

    @Autowired
    private UserService userService;

    @GetMapping("member")
    @PostMapping("member")
    public String member(
            Principal principal, 
            Model model) {
        List<Message> messages = userService.messages(principal.getName());
        model.addAttribute("messages", messages);
        return MEMBER_PATH;
    }

    @PostMapping("new_message")
    protected String newMessage(
            @RequestParam String blabla, 
            Principal principal, 
            Model model)  {

        if(blabla.length() == 0) {
            return REDIRECT_MEMBER_PATH;
        }        

        String username = principal.getName();
        if(blabla.length() <= 140) {
            userService.addMessage(username, blabla);
            return REDIRECT_MEMBER_PATH;
        }
        else {
            model.addAttribute("messages", userService.messages(username));
            return MEMBER_PATH;
        }
    } 


    @PostMapping("del_message")
    protected String delMessage(
            @RequestParam String millis, 
            Principal principal) {

        if(millis != null) {
            userService.deleteMessage(principal.getName(), millis);
        }
        return REDIRECT_MEMBER_PATH;
    }   
}

沒有了 "login" 屬性,那會員頁面中顯示使用者名稱的部份怎麼辦呢?因為目前採用 Thymeleaf 模版,它有個搭配 Spring Security 的方言可以使用,其中有個 sec:authentication 屬性可以使用:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset='UTF-8'>
<title>Gossip 微網誌</title>
<link rel='stylesheet' href='css/member.css' type='text/css'>
</head>
<body>

    <div class='leftPanel'>
        <img src='images/caterpillar.jpg' alt='Gossip 微網誌' /><br>
        <br> <a href='logout'>登出 <span sec:authentication="name">User</span></a>
    </div>
    ... 略
</body>
</html>

為了能使用這個方言,WebConfig 中模版引擎的設定必須新增方言:

package cc.openhome.web;

...略

public class WebConfig implements WebMvcConfigurer, ApplicationContextAware {
    ...略

    @Bean
    public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) {
        // 建立與設定模版引擎
        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.setEnableSpringELCompiler(true);
        engine.setTemplateResolver(templateResolver);
        engine.addDialect(new SpringSecurityDialect());
        return engine;
    }

    ...略  
}

然後,可以修改 SecurityConfig,使用 JDBC 驗證與授權:

package cc.openhome.web;

...略

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter  {
    @Autowired
    private DataSource dataSource;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @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("/gossip/member");
                })
                .failureHandler((request, response, ex) -> {
                    response.sendRedirect("/gossip?username=" + request.getParameter("username") + "&error");
                })
            .and()
                .logout().logoutUrl("/logout")
                .addLogoutHandler((request, response, auth) -> {
                    request.getSession().invalidate();
                    try {
                        response.sendRedirect("/gossip");
                    } 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 PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

必要的相依程式庫必須加入,這就自行查看 build.gradle 的內容吧!你可以在 gossip 中找到以上的範例專案。