如果想讓目前的 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));
}
}
因為不用自行產生鹽值了,建立使用者與重置密碼的部份就簡單許多,而不必要的 login
、encryptedPassword
方法也可以刪掉;因為不需要鹽值了,Account
的 salt
欄位可以去除,AccountDAO
的 updatePasswordSalt
可以修改為 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 中找到以上的範例專案。