源码地址:https://github.com/springsecuritydemo/microservice-auth-center01
无论是在生活还是在 WEB 应用开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能需求,但是应该在应用开启初期就考虑进来。如果在应用开发的后期才考虑安全问题,就可能陷入一个两难的境地:
一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;
两一方面,应用的基本架构已经确定,要修复安全漏洞,可能要对系统的架构做出比较重大的调整,进而需要更多的开发时间,影响整个项目的进度。所以,从应用开发的前期就应该把安全相关的因素考虑进来,并在整个系统开发过程中应用。
一、SpringSecurity简介
SpringSecurity 是基于Spring 提供声明式安全保护的安全性框架。SpringSecurity提供了完整的安全性解决方案,能够在Web请求级别和方法调用级别处理身份认证和授权
1.1 SpringSecurity 如何解决安全性问题 ?
SpringSecurity从两个角度来解决安全性问题:
- 使用 Servlet 规范中的 Filter: 保护web请求并限制 URL 级别的访问;
- 使用 Spring AOP 保护方法调用:借助于动态代理和使用通知,确保只有具备适当权限的用户才能访问安全保护的方法。
1.2 SpringSecurity 模块划分
Spring Security被分为了11个模块:
模块 | 描述 |
---|---|
ACL(access control list) | 支持通过访问控制列表(ACL)为域对象提供安全性 |
切面(Aspects) | 当使用Spring Security注解时,会使用基于AspectJ的切面,而非标准的AOP |
CAS(Central Authentication Service)客户端 | 提供与Jasig的中心认证服务(CAS)进行集成的功能 |
配置(Configuratiion)* | 包含通过XML和Java配置Spring Security的功能支持 |
核心(Core) * | 提供Spring Security基本库 |
加密(Cryptography) | 提供了加密和密码编码的功能 |
LDAP | 支持基于LDAP进行认证 |
OpenID | 支持使用OpenID进行集中式认证 |
Remoting | 提供了对Spring Remoting的支持 |
标签库(Tag Library) | Spring Security的JSP标签库 |
Web | 提供了Spring Security基于Filter的Web安全性支持 |
接下来我们就使用SpringBoot + SpringSecurity完成入门程序。
二、SpringSecurity基本原理
- 绿色部分: 都是项目中根据需求进行自行添加的。
- 蓝色部分:
Exception Translation Filter
在后面的黄色部分抛出权限相关的异常之后会被拦截到,做相应的处理。 - 黄色部分:是对Rest请求服务是否通过认证的最终决定。
- 处理逻辑:当有一个非登录请求过来的时候,会直接进入到黄色部分,在黄色部分验证是否登录,如果登录则放行请求;如果未登录则抛出异常,被蓝色部分拦截后,会重定向到登录页面要求用户登录。在此时如果用户填写用户名和密码单击登录后,请求会被相应的绿色部分的Filter拦截,在Filter中进行用户登录,如果用户登录成功,则会把第一次的请求重定向到后面的
Interceptor
中继续判断是否可以访问REST API。
三、SpringBoot整合SpringSecurity
3.1 导入依赖
导入 spring-boot-starter-security 依赖,在 SpringBoot 2.0.5 环境下默认使用的是 5.0.8 版本。
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-security
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.1
mysql
mysql-connector-java
com.alibaba
druid-spring-boot-starter
1.1.10
org.projectlombok
lombok
1.16.16
3.2 创建数据库表
一般权限控制有三层,即:用户
<–>角色
<–>权限
,用户与角色是多对多,角色和权限也是多对多。这里我们先暂时不考虑权限,只考虑用户
<–>角色
。
这里为了测试,表结构简单设计,后续可以根据业务添加先关字段。
数据库:Mysql 5.6
创建表结构:
-- 用户表
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 角色表
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 用户-角色关系表
CREATE TABLE `sys_user_role` (
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
PRIMARY KEY (`user_id`,`role_id`),
KEY `fk_role_id` (`role_id`),
CONSTRAINT `fk_role_id` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
初始化数据:
INSERT INTO `sys_role` VALUES ('1', 'ROLE_ADMIN');
INSERT INTO `sys_role` VALUES ('2', 'ROLE_USER');
INSERT INTO `sys_user` VALUES ('1', 'admin', '123');
INSERT INTO `sys_user` VALUES ('2', 'pyy', '123');
INSERT INTO `sys_user_role` VALUES ('1', '1');
INSERT INTO `sys_user_role` VALUES ('2', '2');
注意:这里的权限格式为
ROLE_XXX
,是Spring Security规定的,不要乱起名字哦。具体如何修改我们后面讲到权限控制时才做处理。
3.3 准备页面
因为是实例程序,这里页面简单设计,只用于登录的 login.html
以及用户登录成功后跳转的 index.html
,将其放置在工程 resources/static
目录下:
登录login.html:
登陆
登陆
注意:用户的登陆认证是由Spring Security进行处理的,请求路径默认为
/login
,用户名字段默认为username
,密码字段默认为password
首页index.html:
Title
登陆成功
检测ROLE_ADMIN角色
检测ROLE_USER角色
3.4 配置application.yml
server:
port: 9000
servlet:
context-path: /auth
spring:
application:
name: microservice-auth-center
datasource:
druid:
url: jdbc:mysql://localhost:3306/auth_center?characterEncoding=utf-8
username: root
password: 123456
driverClassName: com.mysql.jdbc.Driver
initialSize: 5 #初始建立连接数量
minIdle: 5 #最小连接数量
maxActive: 20 #最大连接数量
maxWait: 10000 #获取连接最大等待时间,毫秒
testOnBorrow: true #申请连接时检测连接是否有效
testOnReturn: false #归还连接时检测连接是否有效
timeBetweenEvictionRunsMillis: 60000 #配置间隔检测连接是否有效的时间(单位是毫秒)
minEvictableIdleTimeMillis: 300000 #连接在连接池的最小生存时间(毫秒)
mybatis:
configuration:
map-underscore-to-camel-case: true #开启Mybatis下划线命名转驼峰命名
3.5 创建实体类、DAO、Service和Controller
实体类:
- SysUser:
@Data
public class SysUser implements Serializable{
private static final long serialVersionUID = -2836223054703407171L;
private Integer id;
private String name;
private String password;
}
- SysRole:
@Data
public class SysRole implements Serializable {
private static final long serialVersionUID = 7510551869226022669L;
private Integer id;
private String name;
}
- SysUserRole:
@Data
public class SysUserRole implements Serializable{
private static final long serialVersionUID = -3256750757278740295L;
private Integer userId;
private Integer roleId;
}
DAO:
- SysUserMapper:
@Mapper
public interface SysUserMapper {
@Select("SELECT * FROM sys_user WHERE id = #{id}")
SysUser selectById(Integer id);
@Select("SELECT * FROM sys_user WHERE name = #{name}")
SysUser selectByName(String name);
}
- SysRoleMapper:
@Mapper
public interface SysRoleMapper {
@Select("SELECT * FROM sys_role WHERE id = #{id}")
SysRole selectById(Integer id);
}
- SysUserRoleMapper:
@Mapper
public interface SysUserRoleMapper {
@Select("SELECT * FROM sys_user_role WHERE user_id = #{userId}")
List listByUserId(Integer userId);
}
Service:
- SysUserService:
@Service
public class SysUserService {
@Autowired
private SysUserMapper userMapper;
public SysUser selectById(Integer id) {
return userMapper.selectById(id);
}
public SysUser selectByName(String name) {
return userMapper.selectByName(name);
}
}
- SysRoleService:
@Service
public class SysRoleService {
@Autowired
private SysRoleMapper roleMapper;
public SysRole selectById(Integer id){
return roleMapper.selectById(id);
}
}
- SysUserRoleService:
@Service
public class SysUserRoleService {
@Autowired
private SysUserRoleMapper userRoleMapper;
public List listByUserId(Integer userId) {
return userRoleMapper.listByUserId(userId);
}
}
Controller:
@Controller
public class LoginController {
private Logger logger = LoggerFactory.getLogger(LoginController.class);
@GetMapping("/login")
public String showLogin() {
return "login.html";
}
@GetMapping("/")
public String showHome() {
String name = SecurityContextHolder.getContext().getAuthentication().getName();
logger.info("当前登陆用户:" + name);
return "index.html";
}
@GetMapping("/admin")
@ResponseBody
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String printAdmin() {
return "如果你看见这句话,说明你有ROLE_ADMIN角色";
}
@GetMapping("/user")
@ResponseBody
@PreAuthorize("hasRole('ROLE_USER')")
public String printUser() {
return "如果你看见这句话,说明你有ROLE_USER角色";
}
}
注意:如代码所示:
- 获取当前登录用户:
SecurityContextHolder.getContext().getAuthentication()
@PreAuthorize
用于判断用户是否有指定权限,没有就不能访问
四、配置SpringSecurity
4.1 自定义用户认证逻辑UserDetailService
首先,我们需要自定义 UserDetailService
, 将用户信息和权限注入进来。
我们需要重写 loadUserByUsername
方法,参数是用户输入的 用户名。 返回值是 UserDetails
,这是一个接口,一般使用它的子类 org.springframework.security.core.userdetails.User
。
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = 500L;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public User(String username, String password, Collection extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection extends GrantedAuthority> authorities) {
if(username != null && !"".equals(username) && password != null) {
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
} else {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
}
这里我们暂时只关注三个参数:用户名、密码和权限集。其它属性暂时使用默认值。
实际情况下,大多将 DAO 中的 User 类继承
org.springframework.security.core.userdetails.User
返回。
CustomUserDetailsService:
@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private SysUserService userService;
@Autowired
private SysRoleService roleService;
@Autowired
private SysUserRoleService userRoleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Collection authorities = new ArrayList<>();
// 从数据库中取出用户信息
SysUser user = userService.selectByName(username);
// 判断用户是否存在
if(user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
// 添加权限
List userRoles = userRoleService.listByUserId(user.getId());
for (SysUserRole userRole : userRoles) {
SysRole role = roleService.selectById(userRole.getRoleId());
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
// 返回UserDetails实现类
return new User(user.getName(), user.getPassword(), authorities);
}
}
4.2 WebSecurityConfig
Spring Security默认是禁用注解的,要想开启注解, 需要在继承WebSecurityConfigurerAdapter
的类上加@EnableGlobalMethodSecurity
注解, 来判断用户对某个控制层的方法是否具有访问权限
例如:上面代码方法前不加@preAuthorize注解,意味着所有用户都能访问方法,如果加上注解,表示只要具备指定角色的用户才有权限访问。
@EnableGlobalMethodSecurity详解:
-
@EnableGlobalMethodSecurity(securedEnabled=true)
开启@Secured
注解过滤权限 -
@EnableGlobalMethodSecurity(jsr250Enabled=true)
开启@RolesAllowed
注解过滤权限 -
@EnableGlobalMethodSecurity(prePostEnabled=true)
使用表达式实现方法级别的安全性 ,4个注解可用:-
@PreAuthorize
在方法调用之前,基于表达式的计算结果来限制对方法的访问 -
@PostAuthorize
允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常 -
@PostFilter
允许方法调用,但必须按照表达式来过滤方法的结果 -
@PreFilter
允许方法调用,但必须在进入方法之前过滤输入值
-
首先,我们将自定义的 userDetailsService
注入进来,在 configure()
方法中使用 auth.userDetailsService()
方法替换掉默认的 userDetailsService
。
这里我们还指定了密码的加密模式(5.0版本强制要求设置),我们采用SpringSecurity提供的加密模式:BCryptPasswordEncoder
,它帮我们实现了PasswordEncoder
,当然也可以自定义加密模式。
package com.thtf.auth.security;
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.method.configuration.EnableGlobalMethodSecurity;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* ========================
* Created with IntelliJ IDEA.
* User:pyy
* Date:2019/7/23 11:10
* Version: v1.0
* ========================
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 如果有允许匿名的url,填在下面
// .antMatchers().permitAll()
.anyRequest().authenticated()
.and()
// 设置登陆页
.formLogin().loginPage("/login")
// 设置登陆成功页
.defaultSuccessUrl("/").permitAll()
// 自定义登陆用户名和密码参数,默认为username和password
// .usernameParameter("username")
// .passwordParameter("password")
.and()
.logout().permitAll();
// 关闭CSRF跨域
http.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
// 设置拦截忽略文件夹,可以对静态资源放行
web.ignoring().antMatchers("/css/**", "/js/**");
}
}
五、运行测试
启动工程之前,由于数据库用户表的密码初始化的是明文,这里我们需要使用SpringSecurity 提供的加密工具类对密码进行重新加密修改:
加密类:
public class SpringSecurityUtil {
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = bCryptPasswordEncoder.encode("123");
System.out.println(password);
}
}
加密后密文:$2a$10$MVN49jp9CwX1M5.8.Sw1NeyrshO.g.zZ7usZ/63B4qbuZxh3NOriG
注:随机盐加密所以两次加密后结果不一样
启动工程:
ROLE_ADMIN 账户:用户名 admin,密码 123
ROLE_USER 账户:用户名 pyy,密码 123
-
使用 ROLE_ADMIN 账号登录:
-
使用 ROLE_USER 账号登录:
到此,我们的SpringBoot + SpringSecurity 入门案例已经讲解完毕,接下来我们将使用SpringSecurity 完成 用户登录 - 自动登录功能。