昨天有一個小錯誤,就是如果我們已經實作UserDetailsService,則Repository部分則不需要重複coding且無須UserDetailsManager,因為已經有UserDetailsService可以取得UserDetails
今天首先先更新SecurityConfig,將原本的inMemoryAuthentication更換為UserDetailsService,其code如下
@EnableWebSecurity //Enable springFliterChain
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DneUserDetailsService dneUserDetailsService;
......
@Override
@Autowired
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
// TODO Auto-generated method stub
auth //Builder Design Pattern
.userDetailsService(dneUserDetailsService); //換成userDetailsService
}
}
接著新增註冊新使用者的form class,命名為RegisterForm,其實也可以用DneUser來替代,只是每個人選擇不一樣,若使用DneUser並假設很多欄位並未在註冊表單中使用到,則可能需要於Controller新增一方法帶有WebDataBinder參數來設定那些欄位允許使用(whitelist)或是那些欄位不可使用(blacklist),有其安全性考量,詳細有文章有說明兩個的利弊,anyway,該form class code如下
import org.hibernate.validator.constraints.NotEmpty;
public class RegisterForm {
@NotEmpty(message="First Name is required")
private String firstName;
@NotEmpty(message="Last Name is required")
private String lastName;
@NotEmpty(message="Username is required")
private String username;
@NotEmpty(message="Password is required")
private String password;
....getter and setter
}
這裡有用到Hibernate Vaildator的Annotation,主要是宣告該欄位不可為空白,當然還有其他的annotation,可詳相關文件,請加入dependency於pom.xml
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.1.2.Final</version>
</dependency>
下一步我們需要新增一個class來取得已經登入的使用者資訊或是將告訴Spring某使用者已經登入,首先建立UserContext介面,接著新增SpringSecurityUserContext實作UserContext,其code如下:
UserContext.java
public interface UserContext {
DneUser getCurrentUser();
void setCurrentUser(DneUser user);
}
SpringSecurityUserContext.java
@Component
public class SpringSecurityUserContext implements UserContext{
@Autowired
private DneUserRepository userRepository;
@Autowired
private DneUserDetailsService dneUserDetailService;
@Override
public DneUser getCurrentUser() { //取得已登入使用者物件
// TODO Auto-generated method stub
SecurityContext context=SecurityContextHolder.getContext(); //從ContextHolder中取得SecurityContext物件
Authentication auth=context.getAuthentication(); //從SecurityContext物件取得Authentication物件
//檢查是否有使用者登入
if(auth== null){
return null;
}
//取得通過驗證的使用者名稱
String username=auth.getName();
return userRepository.findByUserName(username);
}
@Override
public void setCurrentUser(DneUser user) { //將使用者設定已登入
// TODO Auto-generated method stub
//透過Service取得UserDetails
UserDetails userDetails=dneUserDetailService.loadUserByUsername(user.getUsername());
//將使用者放入Authentication物件,代表已通過驗證
Authentication auth=new UsernamePasswordAuthenticationToken(userDetails,
user.getPassword(), userDetails.getAuthorities());
//將Authentication物件放入SecurityContext存放
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
setCurrentUser用在當新的使用者註冊完成後,就登入系統。
到目前為止,我們還缺一個Controller處理使用著註冊及註冊表單網頁,新增RegisterController,其code如下:
@Controller
public class RegisterController {
@Autowired
private UserContext userContext;
@Autowired
private DneUserRepository dneUserRepository;
@RequestMapping(value="/register", method=RequestMethod.GET)
public String register(@ModelAttribute RegisterForm registerForm){
return "registerForm";
}
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegister(@Valid @ModelAttribute RegisterForm registerForm
, BindingResult result){
if(result.hasErrors()){
return "registerForm"; //如果有欄位是空白則回到註冊表單網頁
}
String username=registerForm.getUsername();
if(dneUserRepository.findByUserName(username)!=null){
result.rejectValue("username", "errors.register.username", "The username is already in use");
return "registerForm"; //如果有相同使用者,則回到註冊表單網頁並顯示錯誤訊息
}
DneUser user= new DneUser();
user.setUsername(registerForm.getUsername());
user.setPassword(registerForm.getPassword());
user.setFirstName(registerForm.getFirstName());
user.setLastName(registerForm.getLastName());
dneUserRepository.save(user); //新增使用者到資料庫
userContext.setCurrentUser(user); //設定使用者為已登入
return "redirect:/";
}
}
registerForm.jsp code如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Register A New User</title>
<div class="grid">
<div class="row">
<div class="span1"></div>
<div class="span3">
<form:errors path="*" element="div" cssClass="alert alert-error" />
<form:form modelAttribute="registerForm">
<fieldset>
<legend>New User</legend>
<label>First Name</label>
<div class="input-control text size2">
<form:input type="text" value="" placeholder="" path="firstName" />
<button class="btn-clear" tabindex=-1 type="button"></button>
</div>
<label>Last Name</label>
<div class="input-control text size2">
<form:input type="text" value="" placeholder="" path="lastName" />
<button class="btn-clear" tabindex=-1 type="button"></button>
</div>
<label>Username</label>
<div class="input-control text size3">
<form:input type="text" value="" placeholder="" path="username" />
<button class="btn-clear" tabindex=-1 type="button"></button>
</div>
<label>Password</label>
<div class="input-control text siez3">
<form:input type="password" placeholder="" path="password" />
<button class="btn-clear" tabindex=-1 type="button"></button>
</div>
<input type="submit" value="Add" /> <input type="reset"
value="Clear" />
</fieldset>
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" />
</form:form>
</div>
</div>
</div>
另外更新base.jsp於導覽列加入register連結:
....
<body class="metro">
........
<span class="element-divider place-right"></span>
<a class="element place-right" href="<spring:url value="/register"/>">Register</a>
<span class="element-divider place-right"></span>
</nav>
</nav>
<div>
<sitemesh:write property='body'/>
</div>
啟動Server,畫面如下:
點選Register連結,畫面如下
填入資料後.....發現無法新增使用者....但是login就可以登入,說明UserDetailsService是有效的,可能新增的部分需要debug一下
console log:
21:45:50 [http-nio-8080-exec-65] FilterChainProxy - /login at position 6 of 14 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter'
21:45:50 [http-nio-8080-exec-65] AntPathRequestMatcher - Checking match of request : '/login'; against '/login'
21:45:50 [http-nio-8080-exec-65] UsernamePasswordAuthenticationFilter - Request is to process authentication
21:45:50 [http-nio-8080-exec-65] ProviderManager - Authentication attempt using org.springframework.security.authentication.dao.DaoAuthenticationProvider
Hibernate: select dneuser0_.id as id1_2_, dneuser0_.enabled as enabled2_2_, dneuser0_.firstName as firstNam3_2_, dneuser0_.lastName as lastName4_2_, dneuser0_.password as password5_2_, dneuser0_.username as username6_2_ from DneUser dneuser0_ where dneuser0_.username=?
21:45:50 [http-nio-8080-exec-65] CompositeSessionAuthenticationStrategy - Delegating to org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy@1daf2665
21:45:50 [http-nio-8080-exec-65] CompositeSessionAuthenticationStrategy - Delegating to org.springframework.security.web.csrf.CsrfAuthenticationStrategy@7f435ca7
21:45:50 [http-nio-8080-exec-65] UsernamePasswordAuthenticationFilter - Authentication success. Updating SecurityContextHolder to contain: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@fec65191: Principal: org.springframework.security.core.userdetails.User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN,ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 4D5444E275FFE3F96D726F1B05B43AAF; Granted Authorities: ROLE_ADMIN, ROLE_USER
21:45:50 [http-nio-8080-exec-65] SavedRequestAwareAuthenticationSuccessHandler - Using default Url: /
21:45:50 [http-nio-8080-exec-65] DefaultRedirectStrategy - Redirecting to '/SpringMVC/'