集成Spring Security
本章主要介绍Spring Security的基础知识,Spring Boot如何集成Spring Security,利用Spring Security实现授权登录,以及利用Spring Boot实现数据库数据授权登录等内容。
1.Spring Security概述
在Web应用开发中,安全是非常重要的。因为安全属于应用的非功能性需求,大部分企业更多地会把资源投入到应用开发中,所以应用安全很容易被忽略。安全应该在应用开发的初期就考虑进来,如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并贯穿在整个应用的开发过程中。
市场上开源的安全框架很多,比如Apache Shiro安全框架、Spring Security安全框架等。虽然Spring Security安全框架比Apache Shiro安全框架“重”,但是如果我们用心去了解Spring Security安全框架的话,会发现其实Spring Security安全框架是非常优秀的,所以本书选择在Spring Boot中集成Spring Security安全框架。
Spring Security安全框架除了包含基本的认证和授权功能,还提供了加密解密、统一登录等一系列的支持。Spring Security安全框架简单的实现原理如图所示。

Accessor是资源的访问者,在访问过程中需要经过一系列拦截器Interceptor的拦截,比如FilterSecurityInterceptor、MethodSecurityInterceptor、AspectJSecurityInterceptor等。这些拦截器是统一的抽象类AbstractSecurityInterceptor的具体实现。“控制机构”AccessDecisionManager决定谁可以访问资源,而“身份认证机构”AuthenticationManager就是定义那个“谁”,解决的是访问者身份认证的问题,只有确定注册类,才可以给予访问授权。“控制机构”AccessDecisionManager和“身份认证机构”AuthenticationManager负责制订规则,AbstractSecurityInterceptor负责执行。
2.集成Spring Security的步骤
2.1 引入依赖
在Spring Boot中集成Spring Security,首先需要在pom.xml文件中引入所需的依赖,具体代码如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.2 配置Spring Security
依赖包添加完成之后,在spring-boot-book-v2项目目录/src/main/java/com.example.demo下新建包security,在security包下新建配置类WebSecurityConfig,该类继承WebSecurityConfigurerAdapter类,并在类上添加@EnableWebSecurity注解。WebSecurityConfig具体代码如下:
// security配置类
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigureAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//路由策略和访问权限的简单配置
http
.formLogin() //启用默认登录页面
.failureUrl("/login?error") //登录失败返回rL:/1ogin?error
.defaultSuccessUrl("/ayUser/test") //登录成功跳转URL,这里跳转到用户首页
.permitAll(); //登录页面全部权限可访问
super.configure(http);
}
// 配置内存用户
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.passwordEncoder(new MyPasswordEncoder())
.withUser("阿毅").password("123456").roles("ADMIN")
.and()
.withUser("阿兰").password("123456").roles("USER");
}
}
- @EnableWebSecurity:开启Security安全框架。
- configure方法:WebSecurityConfig继承WebSecurityConfigurerAdapter类需要重写configure方法,在方法中通过formLogin方法配置启用默认登录页面,通过failureUrl方法配置登录失败返回URL,通过defaultSuccessUrl配置登录成功跳转URL,这里调整到用户首页,通过permitAll方法设置登录页面全部权限可访问等。
- configureGlobal方法:参数AuthenticationManagerBuilder类的方法inMemoryAuthentication可添加内存中的用户,并可给用户指定角色权限。比如上面的代码给用户“阿毅”分配了ADMIN权限,而给用户“阿兰”分配了USER权限。
在src/main/java/com.example.demo.security目录下创建MyPasswordEncoder类,具体代码如下:
package com.example.demo.security;
import org.springframework.security.crypto.password.PasswordEncoder;
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence){
return charSequence.tostring();
}
@Override
public boolean matches(CharSequen charSequence, String s){
return s.equals(charSequence.toString());
}
Spring Security的PasswordEncoder接口用于执行密码的单向转换,以便安全地存储密码。简单来说,数据库存储的密码基本都是经过编码的,而决定如何编码以及判断未编码的字符序列和编码后的字符串是否匹配就是PassswordEncoder的责任。
2.3 测试
WebSecurityConfig配置类开发完成,我们重新启动spring-boot-book-v2项目,项目启动成功后,在浏览器中输入访问链接:http://localhost:8080/ayUser/test,该访问请求会被Security框架拦截并跳转到默认的登录界面。在输入框中输入错误的用户名和密码:admin和123456。由于用户名和密码错误,Security框架会跳转到之前在WebSecurityConfig类中配置的错误URL:login?error页面去。


2.4 数据库用户授权登录
在2.2节中,用户登录系统的用户名、密码及角色都是写死在代码,显然不符合正常的逻辑。真正的项目都是通过查询数据库的用户名和密码进行用户认证和授权登录的。首先,需要在数据库中建立角色表ay_role和用户角色关联表ay_user_role_rel,具体的建表语句如下:
-- 角色表
DROP TABLE IF EXISTS `ay_role`;
CREATE TABLE `ay_role`(
`id` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL
);
-- 用户角色关联表
DROP TABLE IF EXISTS `ay_user_role_rel`;
CREATE TABLE `ay_user_role_rel`
`user_id` varchar(255) DEFAULT NULL,
`role_id` varchar(255) DEFAULT NULL
);
角色表ay_role和用户角色关联表ay_user_role_rel建立完成之后,往表里插入数据:
-- 角色表ay_role数据
INSERT INTO `ay role` VALUES ('1', 'ADMIN');
INSERT INTO `ay role` VALUES ('2', 'USER');
-- 用户角色关联表 ay_user_role_rel
INSERT INTO `ay_user_role_rel` VALUES ('1','1')
INSERT INTO `ay_user_role_rel` VALUES ('2','2')
表ay_role和表ay_user_role_rel数据很简单,id为1用户拥有ADMIN角色,id为2用户拥有USER角色。数据插入完成之后,生成对应的实体类:AyRole和AyUserRoleRel,具体代码如下:
// 用户角色
@Entity
@Table(name="ay_role")
public class AyRole {
@Id
private String id;
private String name;
}
// 用户角色关联
@Entity
@Table(name="ay_user_role_rel")
public class AyUserRoleRel {
@Id
private String userId;
private String roleId;
}
实体类AyRole和AyUserRoleRel开发完成之后,我们一样利用JPA生成对应的Repository接口:AyRoleRepository和AyUserRoleRelRepository,具体代码如下:
public interface AyRoleRepository extends JpaRepository<AyRole,String> {
}
public interface AyUserRoleRelRepository extends JpaRepository<AyUserRoleRel,String> {
List<AyUserRoleRel> findByUserId(@Param("userId") String userId);
}
AyUserRoleRelRepository类提供findByUserId方法用来查询用户关联的角色数据。Repository接口AyRoleRepository和AyUserRoleRelRepository开发完成之后,我们生成对应的Service接口:AyRoleService和AyUserRoleRelService。具体代码如下:
public interface AyRoleService {
AyRole find(String id);
}
AyRoleService类提供find接口用来查询AyRole实体。
public interface AyUserRoleRelService {
List<AyUserRoleRel> findByUserId(String userId);
}
AyUserService接口中添加findByUserName接口,用于根据用户名查询具体的用户。
AyUser findByUserName(String name);
在AyUserServiceImpl方法中简单实现findByUserName接口,具体代码如下:
@Override
public AyUser findByUserName(String name) {
List<AyUser> ayUsers = findByName(name)'
if (ayUsers == null && ayUsers.size() <=0) {
return null;
}
return ayUsers.get(0);
}
AyUserRoleRelService类提供findByUserId接口用来查询用户关联角色实体。Service接口AyRoleService和AyUserRoleRelService开发完成之后,我们开发对应的实现类:AyRoleServiceImpl和AyUserRoleRelServiceImpl,在实现类里实现Service接口,并注入对应的Repository接口,具体代码如下:
//用户角色Service
@Service
public class AyRoleServiceImpl implements AyRoleService{
@Resource
private AyRoleRepository ayRoleRepository;
@Override
public AyRole find(String id) {
return ayRoleRepository.findById(id).get();
}
}
@Service
public class AyUserRoleServiceImpl implements AyUserRoleRelService {
@Resource
private AyUserRoleRelRepository ayUserRoleRelRepository;
@Override
public List<AyUserRoleRel> findByUserId(String userId) {
return ayUserRoleRelRepository.findByUserId(userId);
}
}
实现类AyRoleServiceImpl和AyUserRoleRelServiceImpl开发完成之后,我们需要开发CustomUserService类并实现UserDetailsService接口,UserDetailsService接口是Spring Security框架提供的,CustomUserService具体代码如下:
@Service
public class CustomUserService implements UserDetailsService {
@Resource
private AyuserService ayuserService;
@Resource
private AyUserRoleRelService ayUserRoleRelService;
@Resource
private AyRoleService ayRoleService;
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
AyUser ayUser = ayUserService.findByUserName(name);
if(ayUser == null){
throw new BusinessException("用户不存在");
}
//获取用户所有的关联角色
List<AyUserRoleRel> ayRoleList = ayUserRoleRelService.findByUser(ayUser.getId());
List<GrantedAuthority> authorityList = new ArrayList<GrantedAuthority>();
if(ayRoleList !=null && ayRoleList.size()>0) {
for(AyUserRoleRel rel: ayRoleList){
//获取用户关联角色名称
String roleName = ayRoleService.find(rel.getRoleId()).getName();
authorityList.add(new SimpleGrantedAuthority(roleName);
}
}
return new User(ayUser.getName(), ayuser.getPassword(), authorityList);
}
}
在CustomUserService类中注入AyUserService服务类,并在loadUserByUsername方法中通过用户名查询用户。如果用户不存在,抛出业务异常BusinessException,该异常类在第12章中已经开发完成,如果用户存在,继续根据用户Id查询用户关联的角色。最后在loadUserByUsername方法中返回User类,User对象来自org.springframework.security.core. userdetails.User,它实现了UserDetails接口,User类的源代码如下:
public class User implements UserDetails, CredntialsContainer {
private static final long serialVersionUID = 420L;
private String password;
private final String username;
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
// 省略大量代码
}
CustomUserService类开发完成之后,需要在WebSecurityConfig类中注册,WebSecurityConfig具体代码如下:
// security配置类
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigureAdapter {
@Bean
public CustomUserService customUserService(){
return new CustomUserService();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//路由策略和访问权限的简单配置
http
.formLogin() //启用默认登录页面
.failureUrl("/login?error") //登录失败返回rL:/1ogin?error
.defaultSuccessUrl("/ayUser/test") //登录成功跳转URL,这里跳转到用户首页
.permitAll(); //登录页面全部权限可访问
super.configure(http);
}
// 配置内存用户
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(customUserService())
.passwordEncoder(new MyPasswordEncoder());
//.inMemoryAuthentication()
//.passwordEncoder(new MyPasswordEncoder())
//.withUser("阿毅").password("123456").roles("ADMIN")
//.and()
//.withUser("阿兰").password("123456").roles("USER");
}
}
在WebSecurityConfig配置类中,通过注解@Bean将CustomUserService装进Spring容器,并在configureGlobal方法中注册CustomUserService类。
2.5 测试
代码开发完成之后,重新启动spring-boot-book-v2项目,项目启动成功之后,在浏览器中输入访问链接:http://localhost:8080/login,在登录页面中输入用户名和密码:阿毅/123456,单击Login按钮登录,便可以登录成功。关闭浏览器或者清除浏览器的Cookie和缓存信息,重新访问链接:http://localhost:8080/login,在登录页面中输入用户名和密码:阿兰/123456,单击Login按钮登录,同样可以登录成功。