9.1.1 Spring Security 快速入门
1.什么是Spring Security
Spring Security是专门针对基于Spring 的项目的安全框架,充分利用了依赖注入和AOP来实现安全功能。
在早期的Spring Security版本,使用Spring Security需要使用大量的XML配置,而本节将全部基于Java配置来实现Spring Security的功能。
安全框架有两个重要的概念,即认证(Authentication)和授权(Authorization)。认证即确认用户可以访问当前系统;授权即确定用户在当前系统下所拥有的功能权限,本节将围绕认证和授权展开。
2.Security的配置
(1)DelegatingFilterProxy
Spring Security为我们提供了一个多个过滤器来实现所有安全的功能,我们需注册一个特殊的DelegationFilterProxy过滤器到WebApplicationInitializer即可。
而在实际使用中,我们只需让自己的Initializer类继承AbstractSecurityWebApplicationInitializer抽象类即。AbstractSecurityWebApplicationInitializer实现了WebApplicationInitializer接口,并通过onStartup方法调用:
insertSpringSecurityFilterChain(servletContext);
它为我们注册了DelegatingFilterProxy。insertSpringSecurityFilterChain源码如下:
private void insertSpringSecurityFilterChain(ServletContext servletContext) {
String filterName = DEFAULT_FILTER_NAME;
DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy(
filterName);
String contextAttribute = getWebApplicationContextAttribute();
if (contextAttribute != null) {
springSecurityFilterChain.setContextAttribute(contextAttribute);
}
registerFilter(servletContext, true, filterName, springSecurityFilterChain);
}
所以我们只需用以下代码即可开启Spring Security的过滤器支持:
public class AppInitializer extends AbstractSecurityWebApplicationInitializer{
}
(2)配置
Spring Security的配置和Spring MVC的配置类似,只需在一个配置类上注解@EnableWebSecurity,并让这个类继承WebSecurityConfigurerAdapter即可。我们可以通过重写configure方法来配置相关的安全配置。
代码如下:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
super.configure(auth);
}
@Override
public void configure(WebSecurity web) throws Exception {
// TODO Auto-generated method stub
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
super.configure(http);
}
}
3. 用户认证
认证需要我们有一套用户数据的来源,而授权则是对于某个用户有相应的角色权限。在Spring Security里我们通过重写
protected void configure(AuthenticationManagerBuilder auth)
方法来实现定制。
(1)内存中的用户
使用AuthenticationManagerBuilder的inMemoryAuthentication方法即可添加在内存中的用户,并可给用户指定角色权限
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("lmz").password("lmz").roles("ROLE_ADMIN")
.and()
.withUser("wisely").password("wisely").roles("ROLE_USER");
}
(2)JDBC中的用户
JDBC中的用户直接指定dataSource即可。
@Autowired
DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource);
}
不过这看上去很奇怪,其实这里的Spring Security是默认了你的数据库结构的。通过jdbcAuthentication的源码,我们可以看出在jdbcDaoImpl中定义了默认的用户及角色权限获取的SQL语句:
public static final String DEF_USERS_BY_USERNAME_QUERY = "select
username,password,enabled "
+"from users " + "where username = ?";
public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select
username,authority "
+ "from authorities " + "where username = ?";
当然我们可以自定义我们的查询用户和权限的SQL语句,例如:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select username,password,true "
+ "from myusers where username = ?")
.authoritiesByUsernameQuery("select username,role "
+"from roles where username = ?");
}
(3)通用的用户
上面的两种用户和权限的获取方式只限于内存或者JDBC,我们的数据访问方式可以是多种各样的,可以是非关系型数据,也可以是我们常用的JPA等。
这时我们需要定义实现UserDetailsService接口。上在的内存中用户及JDBC用户就是UserDetailsService的实现,定义如下:
@Autowired
SysUserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = userRepository.findByUsername(username);
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
return new User(user.getUsername,user.getPassword(),authorities);
}
说明:SysUser是我们系统的用户领域对象类,User来自于org.springframework.security.core.userdetails.User。
除此之外,我们还需要注册这个CustomUserService,代码如下:
@Bean
UserDetailsService customUserService() {
return new CustomUserService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(customUserService());
}
4.请求授权
Spring Security是通过重写
protected void configure(HttpSecurity http)
方法来实现请求拦截的。
Spring Security使用以下匹配器来匹配请求路径:
antMatchers: 使用Ant风格的路径匹配。
regexMatchers:使用正则表达式匹配路径。
anyRequest:匹配所有请求路径。
在匹配了请求路径后,需要针对当前用户的信息对请求路径进行安全处理,Spring Security提供了下表所示的安全处理方法
方法 | 用途 |
---|---|
access(String) | Spring EL表达式结果为true时可访问 |
anonymous() | 匿名可访问 |
denyAll() | 用户不能访问 |
fullyAuthenticated() | 用户完全认证可访问(非remember me下自动登录) |
hasAnyAuthority(String…) | 如果用户有参数,则其中任一权限可访问 |
hasAnyRole(String…) | 如果用户有参数,则其中任一角色可访问 |
hasAuthority(String) | 如果用户有参数,则其权限可访问 |
hasIpAddress(String) | 如果用户来自参数中的IP则可访问 |
hasRole(String) | 若用户有参数中的角色可访问 |
permitAll() | 用户可以任意访问 |
rememberMe() | 允许通过remember-me登录的用户访问 |
authenticated() | 用户登录后可访问 |
我们可以看一下下面的示例代码:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
authorizeRequests() //通过authorizeRequests方法来开始请求权限配置
.antMatchers("/admin/**").hasRole("ROLE_ADMIN") //请求匹配/admin/**,只有拥有ROLE_ADMIN角色的用户可以访问
.antMatchers("/user/**").hasAnyRole("ROLE_ADMIN","ROLE_USER") //请求匹配/user/**,拥有ROLE_ADMIN或ROLE_USER角色的用户都可访问。
.anyRequest().authenticated(); //其余所有的请求都需要认证后(登录后)才可访问。
}
5.定制登录行为
我们也可以通过重写
prtected void configure(HttpSecurity http)
方法来定制我们的登录行为。
下面将重用的登录行为的定制以简短的代码演示:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin() //通过formLogin方法定制登录操作。
.loginPage("/login") //使用loginPage方法定制登录页面的访问地址
.defaultSuccessUrl("/index") //defaultSuccessUrl指定登录成功后转向的页面
.failureUrl("/login?error") //failureUrl指定登录失败后转向的页面
.permitAll()
.and()
.rememberMe() //rememberMe开启cookie存储用户信息
.tokenValiditySeconds(1209600) //tokenValiditySeconds指定cookie有效期为1209600秒,即2个星期。
.key("myKey") //key指定cookie中的私钥
.and()
.logout() //使用logou方法定制注销行为。
.logoutUrl("/custom-logout") //logoutUrl指定注销的URL路径
.logoutSuccessUrl("/logout-success") //logoutSuccessUrl指定注销成功后转向的页面。
.permitAll();
}
9.1.2 Spring Boot的支持
Spring Boot针对Spring Security的自动配置在org.springframework.boot.autoconfigure.security包中。
主要通过SecurityAutoConfiguration和SecurityProperties来完成配置。
SecurityAutoConfiguration导入了SpringBootWebSecurityConfiguration中的配置。在SpringBootWebSecurityConfiguration配置中,我们获得如下的自动配置:
1)自动配置了一个内存中的用户,账号为user,密码在程序启动时出现。
2)忽略/css/**、/js/**、/images/**和/**/favicon.ico等静态文件的拦截。
3)自动配置的securityFilterChainRegistration的Bean。
SecurityProperties使用以“security”为前缀的属性配置Spring Security相关的配置,包含:
security.user.name=user # 内存中的用户默认账号为user
security.user.password = # 1默认的用户密码
security.user.role=USER # 默认用户的角色
security.require-ssl=false # 是否需要ssl支持
security.enable-csrf-false # 是否开启“跨站请求伪造”支持,默认关闭
security.basic.enabled=true
security.basic.realm=Spring
security.basic.path= # /**
security.basic.authorize-mode=
security.filter-order=0
security.headers.xss=false
security.headers.cache=false
security.headers.frame=false
security.headers.content-type=false
security.headers.hsts=all
security.sessions=stateless
security.ignored= #用逗号隔开的无须拦截的路径
Spring Boot为我们做了如此多的配置,当我们需要自己扩展的配置时,只需配置类继承WebSecurityConfigurerAdapter类即可,无须使用@EnableWebSecurity注解,例如
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
}
9.1.3 实战
在本节的示例中,我们将演示使用Spring Boot下的Spring Security的配置,完成简单的认证授权的功能。此节我们将通过Spring Data JPA获得用户数据。页面模板使用Thymeleaf,Thymeleaf也为我们提供了支持Spring Security的标签。
1.新建Spring Boot项目
新建Spring Boot项目,依赖为JPA(spring-boot-starter-data-jpa)、Security(spring-boot-starter-security)、Thymeleaf(spring-boot-starter-thymeleaf)。
项目信息
groupId:com.wisely
arctifactId:ch9_1
package:com.wisely.ch9_1
并添加Oracle驱动及Thymeleaf的Spring Security的支持。
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.2.0</version>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
我们的application.properties配置如下:
spring.datasource.driverCClassName=oracle.jdbc.OracleDriver
spring.datasource.url=jdbc\:oracle\:thin\:@localhost\:1521\:xe
spring.datasource.username=boot
spring.datasource.password=boot
logging.level.org.springframework.security= INFO
spring.thymeleaf.cache=false
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
将bootstrap.min.css放置在src/main/resources/static/css下,此路径默认不拦截。
2.用户和角色
我们使用JPA来定义用户和角色.
用户:
package com.wisely.ch9_1.domain;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@Entity
public class SysUser implements UserDetails { //让我们的用户实体实现UserDetails接口,我们的用户实体即为Spring Security所使用的用户。
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
private Long id;
private String username;
private String password;
@ManyToMany(cascade= {CascadeType.REFRESH},fetch= FetchType.EAGER) //配置用户和角色的多对多关系。
private List<SysRole> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> auths = new ArrayList<GrantedAuthority>(); //重写getAuthorities方法,将用户的角色作为权限。
List<SysRole> roles = this.getRoles();
for(SysRole role:roles) {
auths.add(new SimpleGrantedAuthority(role.getName()));
}
return auths;
}
@Override
public boolean isAccountNonExpired() { //自动继承的四个bool方法要true,
// TODO Auto-generated method stub
return true;
}
@Override
public boolean isAccountNonLocked() {
// TODO Auto-generated method stub
return true;
}
@Override
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return true;
}
@Override
public boolean isEnabled() {
// TODO Auto-generated method stub
return true;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public List<SysRole> getRoles() {
return roles;
}
public void setRoles(List<SysRole> roles) {
this.roles = roles;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String getPassword() {
// TODO Auto-generated method stub
return password;
}
@Override
public String getUsername() {
// TODO Auto-generated method stub
return username;
}
}
角色:
package com.wisely.ch9_1.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class SysRole {
@Id
@GeneratedValue
private Long id;
private String name; //name为角色名称
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
(1)数据结构及初始化
当我们配置用户和角色的多对多关系后,通过设置
spring.jpa.hibernate.dll-auto=update
为我们自动生成用户表:SYS_USER、角色表:SYS_ROLE、关联表:SYS_USER_ROLES。
针对上面的表结构,我们初始化一些数据来方便我们演示。在src/main/resources下,新建data.sql,即新建两个用户,角色分别为ROLE_ADMIN和ROLE_USER,代码如下
insert into SYS_USER(id,username,password) values (1,'lmz','lmz');
insert into SYS_USER(id,username,password) values (2,'wisely','wisely');
insert into SYS_ROLE(id,name) values(1,'ROLE_ADMIN');
insert into SYS_ROLE(id,name) values(2,'ROLE_USER');
insert into SYS_USER_ROLES(SYS_USER_ID,ROLES_ID) values(1,1);
insert into SYS_USER_ROLES(SYS_USER_ID,ROLES_ID) values(2,2);
(2)传值对象
用来测试不同角色用户的数据展示:
package com.wisely.ch9_1.domain;
public class Msg {
private String title;
private String content;
private String etraInfo;
public Msg(String title,String content,String etraInfo) {
super();
this.title=title;
this.content=content;
this.etraInfo=etraInfo;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getEtraInfo() {
return etraInfo;
}
public void setEtraInfo(String etraInfo) {
this.etraInfo = etraInfo;
}
}
3.数据访问
我们这里的数据访问很简单,代码如下
package com.wisely.ch9_1.dao;
import org.springframework.data.jpa.repository.JpaRepository;
import com.wisely.ch9_1.domain.SysUser;
public interface SysUserRepository extends JpaRepository<SysUser, Long> {
SysUser findByUsername(String username);
}
代码解释:这里只需一个用户名查出用户的方法
4.自定义UserDetailsService
package com.wisely.ch9_1.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import com.wisely.ch9_1.dao.SysUserRepository;
import com.wisely.ch9_1.domain.SysUser;
public class CustomUserService implements UserDetailsService { //自定义需实现UserDetailsService接口
@Autowired
SysUserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) { //重写loadUserByUsername方法获得用户。
SysUser user = userRepository.findByUsername(username);
if(user==null) {
throw new UsernameNotFoundException("用户名不存在");
}
return user; //我们当前的用户实现了UserDetails接口,可直接返回给Spring Security使用。
}
}
5.配置
(1)配置Spring MVC:
package com.wisely.ch9_1.config;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}
}
代码解释:注册访问/login转向login.html页面。
(2)Spring Security 配置:
package com.wisely.ch9_1.config;
import org.springframework.context.annotation.Bean;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import com.wisely.ch9_1.security.CustomUserService;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //扩展Spring Security配置需继承WebSecurityConfigurerAdapter
@Bean
UserDetailsService customUserService() { //注册CustomUserService的Bean
return new CustomUserService();
}
//由于亲版本密码需要加密,这里因为是测试用,所以先忽略密码加码操作
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService()); //添加我们自定义的user detail service认证
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() //所有请求需要认证即登录后才能访问
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.defaultSuccessUrl("/")
.permitAll() //定制登录行为,登录页面可任意访问
.and()
.logout().permitAll(); //定制注销行为,注销请求可以任意访问
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**"); //如果不配置,登录之前静态资源会被拦截
}
}
6.页面
(1)登录页面:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta content="text/html;charset=UTF-8"/>
<title>登录页面</title>
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet" type="text/css" />
<style type="text/css">
body {
padding-top: 50px;
}
.starter-template {
padding: 40px 15px;
text-align: center;
}
</style>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Spring Security演示</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><a th:href="@{/}"> 首页 </a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<div class="container">
<div class="starter-template">
<p th:if="${param.logout}" class="bg-warning">已成功注销</p><!-- 1 -->
<p th:if="${param.error}" class="bg-danger">有错误,请重试</p> <!-- 2 -->
<h2>使用账号密码登录</h2>
<form name="form" th:action="@{/login}" action="/login" method="POST"> <!-- 3 -->
<div class="form-group">
<label for="username">账号</label>
<input type="text" class="form-control" name="username" value="" placeholder="账号" />
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" class="form-control" name="password" placeholder="密码" />
</div>
<input type="submit" id="login" value="Login" class="btn btn-primary" />
</form>
</div>
</div>
</body>
</html>
(2)首页
<!DOCTYPE html>
<!-- Thymeleaf为我们提供的Spring Security的标签支持 -->
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta content="text/html;charset=UTF-8">
<title sec:authentication="name"></title> <!-- 通过sec:authentication="name"来获得当前用户的用户名 -->
<link rel="stylesheet" type="text/css" th:href="@{css/bootstrap.min.css}" />
<style type="text/css">
body{
padding-top:50px;
}
.starter-template {
padding:40px 15px;
text-align:center;
}
</style>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Spring Secrity演示</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><a th:href="@{/}">首页</a></li>
</ul>
</div><!-- /.nav-collapse -->
</div>
</nav>
<div class="container">
<div class="starter-template">
<h1 th:text="${msg.title}"></h1>
<p class="bg-primary" th:text="${msg.content}"></p>
<div sec:authorize="hasRole('ROLE_ADMIN')"> <!-- sec:authorize="hasRole('ROLE_ADMIN')"意味着只有当前用户角色为ROLE_ADMIN时,才可显示标签内容 -->
<p class="bg-info" th:text="${msg.etraInfo}"></p>
</div>
<div sec:authorize="hasRole('ROLE_USER')"><!-- 意味着只有当前用户角色为ROLE_USER时,才可显示标签内内容 -->
<p class="bg-info">无更多信息显示</p>
</div>
<form th:action="@{/logout}" method="POST">
<input type="submit" class="btn btn-primary" value="注销" /> <!-- 注解的默认路径为/logout,需通过POST请求提交 -->
</form>
</div>
</div>
</body>
</html>
7.控制器
此控制器很简单,只为首页显示准备数据:
package com.wisely.ch9_1.web;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.wisely.ch9_1.domain.Msg;
@Controller
public class HomeController {
@RequestMapping("/")
public String index(Model model) {
Msg msg = new Msg("测试标题","测试内容","额外信息,只对管理员显示");
model.addAttribute("msg",msg);
return "home";
}
}
8.运行
(1)登录。访问http://loclhost:8080,将会自动转到登录页面http://localhost:8080/login,如图
使用正确的账号密码登录,如图
使用错误的账号密码登录,如图
(2)注销。登录成功后,单击注册按钮,如图
此时页面显示如图
(3)用户信息
页面上我们将用户名显示在页面的标题上,如图
(4)视图控制
lmz和wisely用户角色不同,因此获得不同的视图。
lmz用户视图如图
wisely用户视图如图