半年前写了篇博文《Spring 3.1 + JPA 2.0 (Hibernate 4) + MySQL配置》,留意到仍有不少访客阅读,最近打算再写一篇关于Spring的文章回馈Spring的爱好者。这篇文章采用了和上篇不同的方式来对Spring相关的技术进行介绍,作者业余时间查阅了不少相关资料,开发实现了网站注册、登录系统,本文以该系统为主线展开。文章涉及到的主要技术有:Spring、 Spring MVC、 Hibernate、EhCache、Spring DATA JPA、Spring Mail、Recapcha、 Spring Security等。
1. Domain:
系统使用了4个类:User(用户)、UserList(保持多个User的链表)、Role(用户的角色)和 Address(用户的地址信息)。
User:User类定义前使用了3个注释(Annotation):@Entity用于说明User本身是一个实体,对应到数据库的一个表格;@Table(name="User")用于说明了数据库表格的名字,不使用@Table(name="User")的话表格的名字和类名保持一致,即User;@Cache注释说明了对数据库表USER进行读写缓存,本文使用了EhCache作为数据库的二级缓存。
用户类包含了下列成员信息:id、firstname、lastname、username、sex、email、password、address、role、confirmed、create和update。
id:该成员使用了@Id、@GeneratedValue、@Column注释。@Id用户说明该成员为USER表的identity列;@GeneratedValue说明该列的值由数据库自动产生;@Column用于自定义该成员在数据库表格中的列名,不使用@Column,数据库表格列名和该成员名保持一致,这一点@Column和上面提到的@Table作用类似。
firstname、lastname:这两个成员都是使用了@Column的nullable = false,说明当向数据库表格插入数据时该成员对应的列不能为空,为空的话数据插入失败。
username:unique = true 说明了数据库中不能存放相同username的记录;updatable = false说明记录一旦插入数据库表格,该值不能进行修改。
address:@ManyToMany定义了User和Address间多对多的映射关系,cascade = CascadeType.ALL说明当USER表格插入一条含有新address的记录时,ADDRESS表格也会自动插入新的记录。@JoinTable用于说明为表格USER和ADDRESS创建一个连接表。
confirmed:该成员用于标示用户注册后有没有打开发送到注册邮箱中的链接进行确认,只有确认后才算成功注册。
confirmedID:该成员是发送到注册邮箱的确认链接的一部分。
create、update:这两个成员分别表示记录的创建和修改时间,@Temporal用于说明是时间戳。注意相关函数onCreate、onUpdate及注释,@PrePersist说明当记录插入数据库时执行,@PreUpdate说明当更新记录时执行。
getCaptcha:该方法比较特殊,只是一个哑方法,用于支持验证错误信息。
package com.haibin.rest.domain;
import java.io.Serializable;
import java.util.Date;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.OneToOne;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import com.haibin.rest.utils.HashCodeUtil;
@Entity
@Table(name = "USER")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User implements Serializable {
private static final long serialVersionUID = 1863930026592012220L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "USER_ID")
private long id;
@Column(name = "USER_FIRSTNAME", nullable = false)
private String firstname;
@Column(name = "USER_LASTNAME", nullable = false)
private String lastname;
@Column(name = "USER_USERNAME", nullable = false, unique = true, updatable = false)
private String username;
@Column(name = "USER_SEX", nullable = false)
private String sex;
@Column(name = "USER_EMAIL", nullable = false, unique = true)
private String email;
@Column(name = "USER_PASSWORD", nullable = false)
private String password;
@Column(name = "USER_ADDRESS")
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "USER_ADDRESS", joinColumns = { @JoinColumn(name = "USER_ID") }, inverseJoinColumns = { @JoinColumn(name = "ADDRESS_ID") })
private Set<Address> address;
@OneToOne(cascade = CascadeType.ALL)
private Role role;
@Column(name = "USER_CONFIRMED")
private boolean confirmed;
@Column(name = "USER_CONFIRMED_ID", unique = true)
private String confirmedID;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "USER_CREATE", nullable = false)
public Date create;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "USER_UPDATE", nullable = false)
private Date update;
public User() {
}
public User(String firstname, String lastname, String sex, String email,
String password, Set<Address> address) {
this.firstname = firstname;
this.lastname = lastname;
this.sex = sex;
this.email = email;
this.password = password;
this.address = address;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
// Dummy getter to support validation error messages
public String getCaptcha() {
return null;
}
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Set<Address> getAddress() {
return address;
}
public void setAddress(Set<Address> address) {
this.address = address;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
public boolean isConfirmed() {
return confirmed;
}
public void setConfirmed(boolean confirmed) {
this.confirmed = confirmed;
}
public String getConfirmedID() {
return confirmedID;
}
public void setConfirmedID(String confirmedID) {
this.confirmedID = confirmedID;
}
public Date getCreate() {
return create;
}
public Date getUpdate() {
return update;
}
@PrePersist
protected void onCreate() {
this.create = this.update = new Date();
}
@PreUpdate
protected void onUpdate() {
this.update = new Date();
}
@Override
public int hashCode() {
int ret = HashCodeUtil.SEED;
ret = HashCodeUtil.hash(ret, id);
ret = HashCodeUtil.hash(ret, firstname);
ret = HashCodeUtil.hash(ret, lastname);
ret = HashCodeUtil.hash(ret, sex);
ret = HashCodeUtil.hash(ret, email);
ret = HashCodeUtil.hash(ret, password);
ret = HashCodeUtil.hash(ret, address);
ret = HashCodeUtil.hash(ret, role);
return ret;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof User)) {
return false;
}
User u = (User) obj;
if (this.id == u.id) {
return true;
} else {
return false;
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("\nID: ");
sb.append(id);
sb.append("\nFirstname: ");
sb.append(firstname);
sb.append("\nLastname: ");
sb.append(lastname);
sb.append("\nSex: ");
sb.append(sex);
sb.append("\nEmail: ");
sb.append(email);
sb.append("\nAddress: ");
sb.append(address.toString());
sb.append("\nRole: ");
sb.append(role.toString());
return sb.toString();
}
}
UserList:该类是普通类(不是实体类@Entity),不在数据库中创建相应表格,仅仅在程序执行过程中使用,用于保存多个用户。
package com.haibin.rest.domain;
import java.util.ArrayList;
import java.util.List;
public class UserList {
private List<User> users = new ArrayList<User>();
public List<User> getUsers() {
return users;
}
public void setUsers(List<User> users) {
users.clear();
users.addAll(users);
}
public void addUsers(List<User> users) {
this.users.addAll(users);
}
public void addUser(User user) {
users.add(user);
}
}
Role:该实体类用于表示用户的角色,管理员或普通用户,后面谈到的Spring Security时再做介绍。
package com.haibin.rest.domain;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;
@Entity
@Table(name = "ROLE")
public class Role implements Serializable {
private static final long serialVersionUID = -3559053024369633882L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "ROLE_ID")
private Long id;
@OneToOne
private User user;
@Column(name = "ROLE_ROLE")
private Integer role;
public Role() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Integer getRole() {
return role;
}
public void setRole(Integer role) {
this.role = role;
}
}
Address:该实体类用户存放用户的地址信息。
package com.haibin.rest.domain;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import com.haibin.rest.utils.HashCodeUtil;
@Entity
@Table(name = "ADDRESS")
public class Address implements Serializable {
private static final long serialVersionUID = 1358043349847785726L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "ADDRESS_ID")
private long id;
@Column(name = "ADDRESS_COUNTRY")
private String country;
@Column(name = "ADDRESS_STATE")
private String state;
@Column(name = "ADDRESS_CITY")
private String city;
@Column(name = "ADDRESS_STREET")
private String street;
@Column(name = "ADDRESS_ZIPCODE")
private String zipCode;
public Address() {
}
public Address(String country, String state, String city, String street,
String zipCode) {
super();
this.country = country;
this.state = state;
this.city = city;
this.street = street;
this.zipCode = zipCode;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getZipCode() {
return zipCode;
}
public void setZipCode(String zipCode) {
this.zipCode = zipCode;
}
@Override
public int hashCode() {
int ret = HashCodeUtil.SEED;
ret = HashCodeUtil.hash(ret, id);
ret = HashCodeUtil.hash(ret, country);
ret = HashCodeUtil.hash(ret, state);
ret = HashCodeUtil.hash(ret, city);
ret = HashCodeUtil.hash(ret, street);
ret = HashCodeUtil.hash(ret, zipCode);
return ret;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof Address)) {
return false;
}
Address a = (Address) obj;
return this.id == a.id && this.country == a.country
&& this.state == a.state && this.city == a.city
&& this.zipCode == a.zipCode;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("\nCountry: ");
sb.append(country);
sb.append("\tState: ");
sb.append(state);
sb.append("\tCity: ");
sb.append(city);
sb.append("\tStreet: ");
sb.append(street);
sb.append("\tZipcode: ");
sb.append(zipCode);
return sb.toString();
}
}
注意:实体类(User、Role、Address)使用了Bean风格的定义,有构造函数、get方法和set方法。
2. Controller:
注册登录系统是基于Spring MVC、Annotation开发的,为了文章篇幅不至于太过于冗长,本文就不再对Spring MVC和Annotation进行深入介绍,请谅解。控制器实体类采用了Restful风格,类定义了CRUD四种操作分别对应creat、get、update和delete四个方法,用于创建用户,读取用户信息、更新用户信息和删除用户。
控制器类都使用了@Controller注释标注。
UserController:该类用于控制用户的创建、用户信息的读取、用户信息的更新及删除用户。
@Controller说明UserController是控制器类;@RequestMapping("/users")用于说明发送到http://domain/users的请求都由控制器UserController进行处理。
注入方式定义了userService成员,userService封装了数据库的各种操作。
@RequestMapping(value = "/records", method = RequestMethod.GET):该注释说明发送到http://domain//users/records的get请求都由getUsers方法进行处理。
create:create方法创建用户并将用户信息保存到数据库。值得注意的一点:数据库并不直接保存用户密码明文信息,而是保存了密码的MD5值,增强了系统的安全性。
package com.haibin.rest.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.haibin.rest.domain.Role;
import com.haibin.rest.domain.User;
import com.haibin.rest.domain.UserList;
import com.haibin.rest.service.UserService;
@Controller
@RequestMapping("/users")
public class UserController {
private static final Logger logger = LoggerFactory
.getLogger(UserController.class);
@Autowired
private UserService userService;
@RequestMapping
public String getUsersPage() {
return "users";
}
// ResponseBody
@RequestMapping(value = "/records", method = RequestMethod.GET)
@ResponseBody
public UserList getUsers() {
return userService.readAll();
}
@RequestMapping(value = "/get", method = RequestMethod.GET)
@ResponseBody
public User get(@RequestBody User user) {
return userService.read(user);
}
@RequestMapping(value = "/create", method = RequestMethod.PUT)
public String create(@RequestParam String firstname,
@RequestParam String lastname, @RequestParam String sex,
@RequestParam String email, @RequestParam String password,
@RequestParam Integer role) {
Role newRole = new Role();
newRole.setRole(role);
User newUser = new User();
newUser.setFirstname(firstname);
newUser.setLastname(lastname);
newUser.setSex(sex);
newUser.setEmail(email);
newUser.setRole(newRole);
StandardPasswordEncoder encoder = new StandardPasswordEncoder();
String hashPass = encoder.encode(password);
logger.debug("{}: hash password is {}", this.getClass(), hashPass);
newUser.setPassword(password);
userService.create(newUser);
return "redirect:registerSuccess.htm";
}
@RequestMapping(value = "/update", method = RequestMethod.POST)
@ResponseBody
public User update(@RequestParam String firstname,
@RequestParam String lastname, @RequestParam String sex,
@RequestParam String email, @RequestParam Integer role) {
Role existingRole = new Role();
existingRole.setRole(role);
User existingUser = new User();
existingUser.setFirstname(firstname);
existingUser.setLastname(lastname);
existingUser.setSex(sex);
existingUser.setEmail(email);
existingUser.setRole(existingRole);
userService.update(existingUser);
return existingUser;
}
@RequestMapping(value = "/delete", method = RequestMethod.DELETE)
@ResponseBody
public Boolean delete(@RequestParam String username) {
User user = new User();
user.setUsername(username);
return userService.delete(user);
}
}
RegisterController:
MA_USER、MA_RECAPTCHA_HMTL:用作model属性的key,将信息从控制器传送到视图。
userRepository:对数据表USER进行CRUD操作的对象。
reCaptcha:防止机器注册的captcha对象。
userValidator:对用户从视图输入的信息进行验证的对象。
mailSender:用于向注册邮箱发送确认连接的发送者对象。
message:邮件对象。
authenticationManager:认证管理器对象。
register():当用户对http://domain/register发送GET请求时,执行register方法,register方法首先创建user、role对象,并进行初始化,user和role对象的其他信息会在用户提交注册表单时获得;生成新的captcha;将user和captcha html对象放进model内,通过return操作传送到register.jsp视图。
registerProcessor():当用户正确填写完注册表单,点击提交按钮时registerProcessor方法被调用。registerProcessor方法首先验证user对象各成员数据是否合法,不合法提示错误信息;检验captcha是否一致,不一致发送新的captcha并重新进入register.jsp页面;产生并设置确认id;产生并设置密码MD5值;构造确认邮件并发送邮件,转向registerSuccess.jsp页面,页面提示用户登录邮箱进行确认,用户只有访问了确认链接后才可登录系统。注意:如果系统允许用户注册后马上自动登录,可以使用注释掉的doAutoLogin()函数并注释后面的构造并发送邮件的代码,修改return值来实现该功能。
确认register():当用户进行确认时,确认register方法被调用,函数通过确认连接中的确认id查找对应用户,查找成功后设置confirmed成员为true,提示注册成功信息并提示登录。查找用户失败,给出失败信息。
doAutoLogin():实现注册完成后自动登录的功能。
package com.haibin.rest.controller;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import net.tanesha.recaptcha.ReCaptcha;
import net.tanesha.recaptcha.ReCaptchaResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import com.haibin.rest.domain.Role;
import com.haibin.rest.domain.User;
import com.haibin.rest.repository.UserRepository;
import com.haibin.rest.validator.UserValidator;
@Controller
public class RegisterController {
private static final Logger logger = LoggerFactory
.getLogger(RegisterController.class);
private static final String MA_USER = "user";
private static final String MA_RECAPTCHA_HTML = "reCaptchaHtml";
@Autowired
private UserRepository userRepository;
@Autowired
private ReCaptcha reCaptcha;
@Autowired
private UserValidator userValidator;
@Autowired
private MailSender mailSender;
@Autowired
private SimpleMailMessage message;
@Autowired
@Qualifier("org.springframework.security.authenticationManager")
private AuthenticationManager authenticationManager;
@RequestMapping(value = "/register", method = RequestMethod.GET)
public String register(Model model) {
User user = new User();
Role role = new Role();
user.setRole(role);
role.setUser(user);
model.addAttribute(MA_USER, user);
String html = reCaptcha.createRecaptchaHtml(null, null);
model.addAttribute(MA_RECAPTCHA_HTML, html);
return "register";
}
@RequestMapping(value = "/register", method = RequestMethod.POST)
public String registerProcessor(HttpServletRequest request,
@RequestParam("recaptcha_challenge_field") String challenge,
@RequestParam("recaptcha_response_field") String response,
@ModelAttribute(MA_USER) User user, BindingResult result,
Model model) throws IOException {
logger.info("{}: challenge = {}", this.getClass(), challenge);
logger.info("{}: response = {}", this.getClass(), response);
userValidator.validate(user, result);
ReCaptchaResponse reCaptchaResponse = reCaptcha.checkAnswer(
request.getRemoteAddr(), challenge, response);
if (!reCaptchaResponse.isValid()) {
result.rejectValue("captcha", "captcha.invalid",
"Please try again. Hover over the red buttons for additional options.");
}
String html = reCaptcha.createRecaptchaHtml(null, null);
model.addAttribute(MA_RECAPTCHA_HTML, html);
if (result.hasErrors()) {
return "register";
}
// Generate a unique confirmation ID for this user
user.setConfirmedID(java.util.UUID.randomUUID().toString());
String password = user.getPassword();
// Store the hash password of plain text password into database.
StandardPasswordEncoder encoder = new StandardPasswordEncoder();
String hashPass = encoder.encode(password);
logger.info("{}: hashed password: {}", this.getClass(), hashPass);
user.setPassword(hashPass);
userRepository.save(user);
// Automatic login after successful registration.
// doAutoLogin(user.getUsername(), password, request);
// Send confirmation email to requestor
StringBuilder sb = new StringBuilder();
sb.append("http://localhost:8080/rest/confirm?id=");
sb.append(user.getConfirmedID());
sendConfirmEmail(user.getEmail(), "Last Step to complete registration",
sb.toString());
return "redirect:/registerSuccess.html";
}
@RequestMapping(value = "/registerSuccess.html", method = RequestMethod.GET)
public String registerSuccessView() {
return "registerSuccess";
}
@RequestMapping(value = "/confirm", method = RequestMethod.GET)
public String register(@RequestParam("id") String id) {
User user = userRepository.findByConfirmedID(id);
if (user == null) {
return "no user corresponds to this confirm link, please register";
}
if (user.isConfirmed()) {
return "user has confirmed";
}
user.setConfirmed(true);
userRepository.save(user);
return "redirect:/confirmSuccess.html";
}
@RequestMapping(value = "/confirmSuccess.html", method = RequestMethod.GET)
public String confirmSuccessView() {
return "confirmSuccess";
}
private void sendConfirmEmail(String to, String subject, String confirmLink) {
message.setTo(to);
message.setSubject(subject);
message.setText(confirmLink);
mailSender.send(message);
}
// Notice: make sure the password input of doAutoLogin() is plain text
@SuppressWarnings("unused")
private void doAutoLogin(String username, String password,
HttpServletRequest request) {
try {
// Must be called from request filtered by Spring Security,
// otherwise SecurityContextHolder is not updated
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
username, password);
// generate session if one doesn't exist
request.getSession();
token.setDetails(new WebAuthenticationDetails(request));
Authentication authentication = this.authenticationManager
.authenticate(token);
SecurityContextHolder.getContext()
.setAuthentication(authentication);
} catch (Exception e) {
SecurityContextHolder.getContext().setAuthentication(null);
logger.error("{}: Failure in doAutoLogin!!!", this.getClass(), e);
}
}
}
MediatorController:该控制器主要起到了跳转功能,完成了apache中.htaccess rewrite的部分工作。如:当用户访问http://domain/home时,会自动跳转到http://domain/home.jsp。
LoginController:该控制器实现了登录功能,比较简单就不详细介绍。
3. Repository ( Spring Data JPA ):
之前的一篇文章介绍的是Spring + JPA 2 + Mysql,如果忽略本文使用的其他技术,那么这篇文章介绍的是 Spring + Spring Data JPA + Mysql。Spring Data JPA的出现进一步简化了数据库访问,作者再次偷懒,不展开介绍Spring Data JPA。如果想了解JPA2 和 Spring Data JPA两种代码风格的区别可以下载查看作者上篇Spring和本篇文章中提供的源码链接,或者网上搜索相关代码。Spring Data JPA提供了访问数据库的几种接口,本文使用的到的是CrudRepository接口,接口中提供了findAll(), save()等默认方法,用户只需定义对应实体类的接口继承CrudRepository即可使用。如果用户使用接口中未提供的方法,比如用户想通过username获得用户信息,可以按照规范提供相应方法的接口即可,如 User findByUsername(String username),注意方法的名字由findBy + 成员名(首字母大写)构成,方法的参数类型和参数名必须和User类定义中的一致。按照规范给出方法签名后,Spring Data JPA会自动完成所希望的功能,只管调用即可,比如UserService中使用UserRepository对数据库进行操作。
4. Service:
UserService:对User的各种操作进行了封装。
package com.haibin.rest.service;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.haibin.rest.domain.Role;
import com.haibin.rest.domain.User;
import com.haibin.rest.domain.UserList;
import com.haibin.rest.repository.RoleRepository;
import com.haibin.rest.repository.UserRepository;
@Service
@Transactional
public class UserService {
private static final Logger logger = LoggerFactory
.getLogger(UserService.class);
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
public User create(User user) {
User existingUser = userRepository.findByUsername(user.getUsername());
if (existingUser != null) {
logger.debug("{}: user has existed", this.getClass());
return null;
}
user.getRole().setUser(user);
User saved = userRepository.save(user);
if (saved == null) {
logger.debug("{}: fail to create", this.getClass());
return null;
}
return saved;
}
public User read(User user) {
return userRepository.findByUsername(user.getUsername());
}
public User update(User user) {
User existingUser = userRepository.findByUsername(user.getUsername());
if (existingUser == null) {
logger.debug("{}:user doesn't exist", this.getClass());
return null;
}
// Only firstname, lastname, and role fields are updatable
existingUser.setFirstname(user.getFirstname());
existingUser.setLastname(user.getLastname());
existingUser.getRole().setRole(user.getRole().getRole());
Role roleSaved = roleRepository.save(existingUser.getRole());
if (roleSaved == null) {
logger.debug("{}:fail to update role", this.getClass());
return null;
}
User saved = userRepository.save(existingUser);
if (saved == null) {
logger.debug("{}:fail to update user", this.getClass());
return null;
}
return saved;
}
public boolean delete(User user) {
User existingUser = userRepository.findByUsername(user.getUsername());
if (existingUser == null) {
logger.debug("{}:user doesn't exist", this.getClass());
return false;
}
userRepository.delete(existingUser);
return true;
}
public UserList readAll() {
UserList userList = new UserList();
List<User> list = new ArrayList<User>();
Iterable<User> users = userRepository.findAll();
for (User user : users) {
list.add(user);
}
userList.setUsers(list);
return userList;
}
}
CustomUserDetailsService:使用Spring Security需要实现的服务。
package com.haibin.rest.service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.haibin.rest.repository.UserRepository;
@Service
@Transactional(readOnly = true)
public class CustomUserDetailsService implements UserDetailsService {
private static final Logger logger = LoggerFactory
.getLogger(CustomUserDetailsService.class);
@Autowired
private UserRepository userRepository;
/**
* Returns a populated {@link UserDetails} object. The useremail is first
* retrieved from the database and then mapped to a {@link UserDetails}
* object.
*/
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
// Search database for a user that matches the specified username
com.haibin.rest.domain.User user = userRepository
.findByUsername(username);
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
if (user == null) {
logger.info("{}: no such user exists", this.getClass());
throw new UsernameNotFoundException(username);
}
else if(user.isConfirmed() == false) {
logger.info("{}: user doesn't confirm", this.getClass());
throw new UsernameNotFoundException(username);
}
// Populate the Spring User object with details from database user.
return new User(user.getUsername(), user.getPassword().toLowerCase(),
enabled, accountNonExpired, credentialsNonExpired,
accountNonLocked, getAuthorities(user.getRole().getRole()));
}
/**
* Retrieves a collection of {@link GrantedAuthority} based on a numerical
* role
*
* @param role
* the numerical role
* @return a collection of {@link GrantedAuthority
*/
public Collection<? extends GrantedAuthority> getAuthorities(Integer role) {
List<GrantedAuthority> authList = getGrantedAuthorities(getRoles(role));
return authList;
}
/**
* Converts a numerical role to an equivalent list of roles
*
* @param role
* the numerical role
* @return list of roles as as a list of {@link String}
*/
public List<String> getRoles(Integer role) {
List<String> roles = new ArrayList<String>();
if (role.intValue() == 1) {
roles.add("ROLE_USER");
roles.add("ROLE_ADMIN");
} else if (role.intValue() == 2) {
roles.add("ROLE_USER");
}
return roles;
}
/**
* Wraps {@link String} roles to {@link SimpleGrantedAuthority} objects
*
* @param roles
* {@link String} of roles
* @return list of granted authorities
*/
public static List<GrantedAuthority> getGrantedAuthorities(
List<String> roles) {
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
}
5. Validator:
UserValidator:对用户提供的信息进行验证,没有通过验证的给出出错信息,出错信息定义在src/main/resources/messages.properties文件内。
6. 配置:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<!-- Enables support for DELETE and PUT request methods with web browser
clients -->
<filter>
<filter-name>hiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Spring Security -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- The definition of the Root Spring Container shared by all Servlets
and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/spring-security.xml
/WEB-INF/spring/root-context.xml
</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- Disables Servlet Container welcome file handling. Needed for compatibility
with Servlet 3.0 and Tomcat 7.0 -->
<welcome-file-list>
<welcome-file></welcome-file>
</welcome-file-list>
</web-app>
servlet-context.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing
infrastructure -->
<!-- Resolves views selected for rendering by @Controllers to .jsp resources
in the /WEB-INF/views directory -->
<beans:bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/views/" p:suffix=".jsp" />
</beans:beans>
root-context.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd">
<!-- Activates the Spring infrastructure for various annotations to be detected
in bean classes: Spring's @Required and @Autowired, as well as JSR 250's
@PostConstruct, @PreDestroy and @Resource (if available), and JPA's @PersistenceContext
and @PersistenceUnit (if available). Alternatively, you can choose to activate
the individual BeanPostProcessors for those annotations explictly. -->
<!-- <context:annotation-config /> -->
<!-- Root Context: defines shared resources visible to all other web components -->
<!-- Scans within the base package of the application for @Components to
configure as beans, context:component-scan automatically includes context:annotation-config, so context:annotation-config is needless -->
<!-- @Controller, @Service, @Repository, @Configuration, etc -->
<context:component-scan base-package="com.haibin.rest" />
<!-- Enables the Spring MVC @Controller programming model -->
<mvc:annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving
up static resources in the ${webappRoot}/resources directory -->
<mvc:resources mapping="/resources/**" location="/resources/" />
<!-- Maps '/' requests to the 'home' view -->
<mvc:view-controller path="/" view-name="home"/>
<mvc:default-servlet-handler/>
<bean id="reCaptcha" class="net.tanesha.recaptcha.ReCaptchaImpl"
p:publicKey="***" p:privateKey="***" />
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource"
p:basename="messages" />
<import resource="spring-data.xml" />
<import resource="spring-mail.xml" />
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:cache="http://www.springframework.org/schema/cache" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache-3.1.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
http://www.springframework.org/schema/data/jpa
http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-3.1.xsd">
<!-- Data Source for Development -->
<!--
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/hibernate" />
<property name="username" value="root" />
<property name="password" value="" />
</bean>
-->
<!-- Data Source for Production -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close"
p:driverClass="com.mysql.jdbc.Driver"
p:jdbcUrl="jdbc:mysql://localhost:3306/hibernate"
p:user="root"
p:password=""
p:initialPoolSize="5"
p:maxPoolSize="100"
p:minPoolSize="10"
p:maxStatements="50"
p:acquireIncrement="5"
p:idleConnectionTestPeriod="60"
/>
<!-- JPA Entity Manager Factory -->
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
p:packagesToScan="com.haibin.rest" p:dataSource-ref="dataSource"
p:jpaVendorAdapter-ref="hibernateVendor" p:jpaPropertyMap-ref="jpaPropertyMap" />
<bean id="hibernateVendor"
class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"
p:database="MYSQL" p:showSql="true" p:generateDdl="true"
p:databasePlatform="org.hibernate.dialect.MySQLDialect" />
<util:map id="jpaPropertyMap">
<entry key="hibernate.hbm2ddl.auto" value="update" />
<entry key="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" />
<!-- To enable Hibernate's second level cache and query cache settings -->
<entry key="hibernate.max_fetch_depth" value="4" />
<entry key="hibernate.cache.use_second_level_cache" value="true" />
<entry key="hibernate.cache.use_query_cache" value="true" />
<!-- NOTE: if using net.sf.ehcache.hibernate.EhCacheRegionFactory for Hibernate 4+, probrems happen -->
<entry key="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory" />
</util:map>
<!-- EhCache Configuration -->
<cache:annotation-driven />
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"
p:cacheManager-ref="ehcache" />
<!-- NOTE: make sure p:share="true", or problems happen -->
<bean id="ehcache"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
p:configLocation="/WEB-INF/classes/ehcache.xml" p:shared="true" />
<!-- Transaction Config -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"
p:entityManagerFactory-ref="entityManagerFactory">
<property name="jpaDialect">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" />
</property>
</bean>
<!-- User declarative transaction management -->
<tx:annotation-driven transaction-manager="transactionManager" />
<!-- This will ensure that Hibernate or JPA exceptions are automatically
translated into Spring's generic DataAccessException hierarchy for those
classes annotated with Repository. -->
<bean
class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<!-- Activate Spring Data JPA repository support -->
<jpa:repositories base-package="com.haibin.rest.repository" />
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false"
monitoring="autodetect" dynamicConfig="true">
<diskStore path="java.io.tmpdir" />
<defaultCache
maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
maxEntriesLocalDisk="10000000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
<cache name="org.hibernate.cache.UpdateTimestampsCache"
maxElementsInMemory="5000"
eternal="true"
overflowToDisk="true"
/>
<cache name="org.hibernate.cache.StandardQueryCache"
maxElementsInMemory="5000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
<cache name="com.haibin.rest.domain.User"
maxEntriesLocalHeap="50"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
/>
</ehcache>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="smtp.gmail.com" />
<property name="port" value="465" />
<property name="username" value="***@gmail.com" />
<property name="password" value="***"/>
<property name="protocol" value="smtps" />
<property name="javaMailProperties">
<props>
<!-- Use SMTP-AUTH to authenticate to SMTP server -->
<prop key="mail.smtps.auth">true</prop>
<!-- User TLS to encrypt communication with SMTP server -->
<prop key="mail.smtps.starttls.enable">true</prop>
<prop key="mail.debug">true</prop>
</props>
</property>
</bean>
<bean id="message" class="org.springframework.mail.SimpleMailMessage" />
</beans>
spring-security.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<http pattern="/resources/**" security="none" />
<http auto-config="true" use-expressions="true" disable-url-rewriting="true">
<intercept-url pattern="/register" access="permitAll" />
<intercept-url pattern="/registerSuccess" access="permitAll" />
<intercept-url pattern="/confirmSuccess" access="permitAll" />
<intercept-url pattern="/login" access="permitAll" />
<intercept-url pattern="/logout" access="permitAll" />
<intercept-url pattern="/denied" access="hasRole('ROLE_USER')" />
<intercept-url pattern="/" access="hasRole('ROLE_USER')" />
<intercept-url pattern="/user" access="hasRole('ROLE_USER')" />
<intercept-url pattern="/admin" access="hasRole('ROLE_ADMIN')" />
<form-login login-page="/login" authentication-failure-url="/login/failure"
default-target-url="/" />
<access-denied-handler error-page="/denied" />
<logout invalidate-session="true" logout-success-url="/logout/success"
logout-url="/logout" />
</http>
<beans:bean id="encoder"
class="org.springframework.security.crypto.password.StandardPasswordEncoder">
</beans:bean>
<!-- Declare an authentication-manager to user a custom userDetailService -->
<authentication-manager>
<authentication-provider user-service-ref="customUserDetailsService">
<password-encoder ref="encoder" />
</authentication-provider>
</authentication-manager>
</beans:beans>