辛苦堆砌,转载请注明出处,谢谢!
之前的User校验我们自己通过比较用户名和密码来完成,这样可能存在一些安全隐患,还需要自己处理Session的问题。本篇文章使用Spring Security进行安全校验,对项目进行重构。
Spring Security是Spring实现的安全框架,可以对请求和方法进行安全保护,Spring Security根本上是一套Filter链,当配置使用Spring Security时,Spring会向项目中添加Filter,从而对请求进行拦截,并进行必要的安全校验。
首先,为了让Spring支持Spring Security,需要添加必要的依赖,这是因为Spring Security不属于Spring Framework,是一个独立的项目。
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring-security-web.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring-security-web.version}</version>
</dependency>
然后,创建如下的类
package com.yjp.springmvc.blog.config;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {
}
该类派生自AbstractSecurityWebApplicationInitializer,这样,当启用Spring Security时,会加入安全校验需要的Filter链。
最后,启用Spring Security,并配置拦截规则和用户数据源
package com.yjp.springmvc.blog.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置拦截规则
http
.formLogin()
.loginPage("/login")
.failureUrl("/loginError")
.and()
.logout()
.logoutSuccessUrl("/login")
.and()
.authorizeRequests()
.antMatchers("/").authenticated()
.antMatchers("/**").permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService);
}
}
我们派生了WebSecurityConfigurerAdapter类,并重新实现了两个方法,其中,configure以HttpSecurity为参数的方法,用来定制拦截规则,我们这里拦截"/"请求,其他请求放行,然后通过表单认证,认证表单为login,验证失败进入loginError请求,注销时,默认会进入logout请求,注销成功跳转到login请求。通过
configure以AuthenticationManagerBuilder为参数的方法,我们返回数据源,数据源Spring会从UserDetailsService获取。记得将该配置类引入到RootConfig中。
下面调整我们之前的项目,删除LoginController,创建SecurityController
package com.yjp.springmvc.blog.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class SecurityController {
@RequestMapping(method=RequestMethod.GET, value="/login")
public String login(String error) {
return "login";
}
@RequestMapping(method=RequestMethod.GET, value="/loginError")
public String loginError(Model model) {
model.addAttribute("error", "用户名或密码错误");
return "login";
}
}
主要用来处理login流程的请求,将请求与对应的视图挂钩,看看login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page isELIgnored="false"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>简微</title>
<link rel="stylesheet" type="text/css" href="resources/css/login.css">
<link rel="stylesheet" type="text/css" href="resources/css/error.css">
</head>
<body>
<div class="loginPanel">
<div>
<img src="resources/images/logo.png" alt="简微"/>
</div>
<form method="post" action="login">
<table>
<tr>
<td colspan="2" align="center" style="font-weight:bold">会员登录</td>
</tr>
<tr>
<td>名称:</td>
<td><input type="text" name="username"/></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"/></td>
</tr>
<tr>
<td colspan="2" align="center"><span class="error">${error}</span></td>
</tr>
<tr>
<td colspan="2" align="center"><input type="submit" value="登入"></td>
</tr>
<tr>
<td align="center"><a href="registerPage">注册</a></td>
<td align="center"><a href="forgotPage">忘记密码?</a></td>
</tr>
</table>
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>
</div>
<div>
<h1>简微</h1>
<ul>
<li>说你想说</li>
<li>看你想看</li>
<li>就这么简单</li>
</ul>
</div>
</body>
</html>
删除了之前用Spring标签库做的内容,表单发送POST请求到login,这里千万注意,我们拦截规则的formPage中的/login是GET请求,会发送到我们的SecurityController处理,然后返回login.jsp视图,而视图表单中的login是POST给Spring Security处理的,会完成及鉴权相关的工作。另外,编单中添加了一个
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
这个是防止CSRF攻击的,Spring Security要求要有这个表单域。这样,如果我们输入的用户名密码与数据源比较通过校验,就会进入"/"请求,我们在HomeController中处理该请求
package com.yjp.springmvc.blog.web.controller;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class HomeController {
@RequestMapping(method=RequestMethod.GET, value="/")
public String home(Model model) {
//通过Spring Security获取当前的用户
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal();
model.addAttribute("username", userDetails.getUsername());
return "home";
}
}
该请求会跳转到home.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ page isELIgnored="false"%>
<!DOCTYPE html">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>简微</title>
<link rel="stylesheet" href="resources/css/home.css" type="text/css">
</head>
<body>
<div class="leftPanel">
<img src="resources/images/logo.png" alt="简微" /><br><br>
<form method="post" action="logout" style="">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<button type="submit">注销 ${username}</button>
</form>
</div>
<form method="post" action="message">
说说你的事...<br><br>
<textarea cols='60' rows='4' name='blabla'></textarea><br><br>
<button type="submit">送出</button>
</form>
</body>
</html>
这里的home.jsp我们已经完成了注销功能,注意有一个发送POST logout请求的表单,这个同样的道理,注销流程会交给Spring Security处理,主要是清除及鉴权相关的数据。
最后,由于我们要给鉴权提供数据源,持久层做了相应的改动,首先看看UserService
package com.yjp.springmvc.blog.beans.service;
import org.springframework.security.core.userdetails.UserDetailsService;
import com.yjp.springmvc.blog.beans.model.User;
public interface UserService extends UserDetailsService {
boolean saveUser(User user);
}
package com.yjp.springmvc.blog.beans.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.yjp.springmvc.blog.beans.model.User;
import com.yjp.springmvc.blog.beans.repository.UserRepository;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public boolean saveUser(User user) {
User saveUser = userRepository.save(user);
return saveUser != null;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserDetails findUser =
userRepository.findUserByUsername(username);
if (findUser != null) {
return findUser;
} else {
return null;
}
}
}
UserService实现了UserDetailsService接口,用来提供数据源,我们的数据源就是User对象,但是User对象实现了UserDetails接口
package com.yjp.springmvc.blog.beans.model;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.GenericGenerator;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@Entity
@Table(name="users")
public class User implements Serializable, UserDetails {
private static final long serialVersionUID = 9038460243059691075L;
@Id
@GenericGenerator(strategy = "assigned", name = "username")
private String username;
@Column
private String password;
@Column
private String email;
public User() {}
public User(String username, String password, String email) {
this.username = username;
this.password = password;
this.email = email;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//返回用户权限
return Arrays.asList(new SimpleGrantedAuthority("USER"));
}
@Override
public boolean isAccountNonExpired() {
//账户是否会过期
return true;
}
@Override
public boolean isAccountNonLocked() {
//用户是否被锁定
return true;
}
@Override
public boolean isCredentialsNonExpired() {
//密码是否会过期
return true;
}
@Override
public boolean isEnabled() {
//用户是否使能
return true;
}
}
UserDetails接口的方法已经添加注释,可以酌情修改。这样就完成了Spring Security的配置,并将其使用在了我们的项目中。