- 前言
- 第一部分 基础应用开发
- 第 1 章 Spring Boot 入门
- 第 2 章 在 Spring Boot 中使用数据库
- 第 3 章 Spring Boot 界面设计
- 第 4 章 提高数据库访问性能
- 第 5 章 Spring Boot 安全设计
- 第二部分 分布式应用开发
- 第 6 章 Spring Boot SSO
- 第 7 章 使用分布式文件系统
- 第 8 章 云应用开发
- 第 9 章 构建高性能的服务平台
- 第三部分 核心技术源代码分析
- 第 10 章 Spring Boot 自动配置实现原理
- 第 11 章 Spring Boot 数据访问实现原理
- 第 12 章 微服务核心技术实现原理
- 附录 A 安装 Neo4j
- 附录 B 安装 MongoDB
- 附录 C 安装 Redis
- 附录 D 安装 RabbitMQ
- 结束语
5.3 登录认证设计
完成上面的安全策略配置之后,打开受保护的页面或链接时,就会引导用户到登录页面上输入用户名和密码验证用户身份。如果在安全配置中没有指定登录页面 URL,Spring Security 就调用其默认的登录页面。只是,Spring Security 的登录页面设计很简单,不适合于一般的 Web 应用的登录设计。除了登录页面,Spring Security 对于用户身份验证同样也已经实现了,只需要加以引用即可。
5.3.1 用户实体建模
可以使用第 2 章的实例工程 MySQL 模块的实体建模来建立用户体系,回顾一下,在第 2 章中建模的实体中包含用户、部门和角色三个对象,它们的关系是,一个用户只能属于一个部门,一个用户可以拥有多个角色,这非常适合本章的实例。除了部门和角色,用户实体的属性必须做些调整,以适合本章实例的要求,如代码清单 5-11 所示,即增加了邮箱、性别和密码等几个属性,其他基本相同。
代码清单 5-11 用户实体建模
@Entity
@Table(name = "user")
public class User implements java.io.Serializable{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private Integer sex;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createdate;
private String password;
@ManyToOne
@JoinColumn(name = "did")
@JsonBackReference
private Department department;
@ManyToMany(cascade = {}, fetch = FetchType.EAGER)
@JoinTable(name = "user_role",
joinColumns = {@JoinColumn(name = "user_id")},
inverseJoinColumns = {@JoinColumn(name = "roles_id")})
private List<Role> roles;
......
}
另外,在用户实体的持久化方面,也增加了几个方法以便能适用本章实例的要求,如代码清单 5-12 所示。其中 User findByName(String name)就是登录时使用用户名来查询用户的信息。
代码清单 5-12 用户实体持久化接口
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select t from User t where t.name =?1 and t.email =?2")
User findByNameAndEmail(String name, String email);
@Query("select t from User t where t.name like :name")
Page<User> findByName(@Param("name") String name, Pageable pageRequest);
User findByName(String name);
}
5.3.2 用户身份验证
在安全配置类的定义中,使用了如代码清单 5-13 所示的配置,用来调用我们自定义的用户认证 CustomUserDetailsService,并且指定了使用密码的加密算法为 BCryptPasswordEncoder,这是 Spring Security 官方推荐的加密算法,比 MD5 算法的安全性更高。
代码清单 5-13 安全配置类引用
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(customUserDetailsService).passwordEncoder
(passwordEncoder());
// remember me
auth.eraseCredentials(false);
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
如代码清单 5-14 所示,CustomUserDetailsService 实现了 Spring Security 的 User-DetailsService,重载了 loadUserByUsername(String userName),并返回自定义的 Security-User,通过这个 SecurityUser 来完成用户的身份认证。其中,loadUserByUsername 调用了用户资源库接口的 findByName 方法,取得登录用户的详细信息。
代码清单 5-14 CustomUserDetailsService 定义
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNot
FoundException {
User user = userRepository.findByName(userName);
if (user == null) {
throw new UsernameNotFoundException("UserName " + userName + "
not found");
}
return new SecurityUser(user);
}
}
SecurityUser 继承于实体对象 User,并实现了 Spring Security 的 UserDetails,同时重载了 getAuthorities(),用来取得为用户分配的角色列表,用于后面的权限验证,它的实现如代码清单 5-15 所示。
代码清单 5-15 SecurityUser 定义
public class SecurityUser extends User implements UserDetails
{
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<Granted
Authority>();
List<Role> roles = this.getRoles();
if(roles != null)
{
for (Role role : roles) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority
(role.getName());
authorities.add(authority);
}
}
return authorities;
}
......
}
5.3.3 登录界面设计
首先创建一个登录控制器,编写如代码清单 5-16 所示的代码,这个控制器很简单,它仅仅是返回对一个页面的调用,页面的设计文件是 login.html。
代码清单 5-16 登录控制器
@Controller
public class LoginController {
@RequestMapping("/login")
public String login(){
return "login";
}
}
登录界面的设计在页面文件 login.html 中完成,代码清单 5-17 是表单设计的部分代码和一些错误提示设计。表单中设置了用户、密码和验证码等输入框,最终使用 POST 方式提交,提交的链接地址是/loging,这将请求 Spring Security 的内部方法。
代码清单 5-17 登录界面表单设计
<div th:if="${param.error}">
<input th:value="无效的用户或密码!
" id="errorMsg" type="hidden"/>
</div>
<div th:if="${param.logout}">
<input th:value="你已经退出!
" id="errorMsg" type="hidden"/>
</div>
<div th:if="${#httpServletRequest.remoteUser != null}">
<input th:value="${#httpServletRequest.remoteUser}" id="errorMsg"
type="hidden"/>
</div>
<form th:action="@{/login}" id="loginForm" method="post">
<div class="loginTit png"></div>
<ul class="infList">
<li class="grayBox">
<label for="username" class="username-icon"></label>
<input id="username" class="username" name="username"
type="text" placeholder="您的用户名
"/>
<div class="close png hide"></div>
</li>
<li class="grayBox">
<label class="pwd-icon" id="pwd"></label>
<input id="password" name="password" class="pwd" type=
"password" placeholder="登录密码
"/>
<div class="close png hide"></div>
</li>
<li class="">
<label class="validateLabel" ></label>
<input id="checkCode" name="checkCode" class="checkCode"
type="text" placeholder="验证码
" />
<img onclick="reloadImg();"
th:src="@{/images/imagecode}" id="validateImg" alt="验证码
" class="codePic" title=
"验证码。单击此处更新验证码。
"/>
<a class="getOther" href="javascript:void(0);" onclick=
"reloadImg();" title="单击此处可以更新验证码。
">更新
</a>
</li>
</ul>
<ul class="infList reloadBtn">
<li>
<a href="javascript:void(0);" onclick="tologin();">本页面已经失效。请单击此处重新登录。
</a>
</li>
</ul>
<div class="loginBtnBox">
<div class="check-box"><input type="hidden" value="0" id=
"remember-me" name="remember-me" onclick="if(this.checked){this.value = 1}
else{this.value=0}" /><span class="toggleCheck no-check" id="repwd"></span>记住
我
</div>
<input type="button" id="loginBtn" onclick="verSubmit()"
value="登录
" class="loginBtn png" />
</div>
</form>
完成的登录界面设计效果如图 5-1 所示。

图 5-1 登录界面设计效果图
5.3.4 验证码验证
注意到上面登录界面设计中有一个验证码功能,这个功能 Spring Security 是没有的,必须由我们来实现。代码清单 5-18 是使用验证码的实现代码,其中 imagecode 方法是一个生成图形验证码的请求,checkcode 方法实现了对这个图形验证码的验证。从验证码的生成到验证的过程中,验证码是通过 Session 来保存的,并且设定一个验证码的最长有效时间为 5 分钟。验证码的生成规则是从 0~9 的数字中,随机产生一个 4 位数,并增加一些干扰元素,最终组合成为一个图形输出。
代码清单 5-18 验证码验证
@RequestMapping(value = "/images/imagecode")
public String imagecode(HttpServletRequest request, HttpServletResponse response)
throws Exception {
OutputStream os = response.getOutputStream();
Map<String,Object> map = ImageCode.getImageCode(60, 20, os);
String simpleCaptcha = "simpleCaptcha";
request.getSession().setAttribute(simpleCaptcha, map.get("strEnsure").
toString().toLowerCase());
request.getSession().setAttribute("codeTime",new Date().getTime());
try {
ImageIO.write((BufferedImage) map.get("image"), "JPEG", os);
} catch (IOException e) {
return "";
}
return null;
}
@RequestMapping(value = "/checkcode")
@ResponseBody
public String checkcode(HttpServletRequest request, HttpSession session)
throws Exception {
String checkCode = request.getParameter("checkCode");
Object cko = session.getAttribute("simpleCaptcha") ; // 验证码对象
if(cko == null){
request.setAttribute("errorMsg", "验证码已失效,请重新输入!
");
return "验证码已失效,请重新输入!
";
}
String captcha = cko.toString();
Date now = new Date();
Long codeTime = Long.valueOf(session.getAttribute("codeTime")+"");
if(StringUtils.isEmpty(checkCode) || captcha == null || !(checkCode.
equalsIgnoreCase(captcha))){
request.setAttribute("errorMsg", "验证码错误!
");
return "验证码错误!
";
}else if ((now.getTime()-codeTime)/1000/60>5){// 验证码有效时长为
5 分钟
request.setAttribute("errorMsg", "验证码已失效,请重新输入!
");
return "验证码已失效,请重新输入!
";
}else {
session.removeAttribute("simpleCaptcha");
return "1";
}
}
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论