SpringSecurity原理剖析及其实战(一)
https://blog.csdn.net/weixin_43934626/article/details/121050704
1.Spring Security介绍
1.1 Spring Security定义
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。Spring Security主要实现了Authentication(认证,解决who are you?)和AccessControl(访问控制,也就是what are you allowed to do?,也称为Authorization)。Spring Security在架构上将认证与授权分离,并提供了扩展点。
FROM 《Spring Security 官网》
Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications. Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于 Spring 的 应用程序。
Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements Spring Security 是一 个框架,侧重于为 Java 应用程序提供身份验证和授权。与所有 Spring 项目一样,Spring 安全性 的真正强大之处,在于它很容易扩展以满足定制需求。
认证(Authentication):用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式用:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。
授权(Authorization):授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。
1.2Spring Security和Shiro比较
在java生态中,目前有Spring Security和Apache shiro两个安全框架,可以完成认证和授权的功能。
- Spring Security
- Apache Shiro: 一个功能强大且易于使用的java安全框架,提供了认证,授权,加密,和会话管理。
相同点:
1:认证功能
2:授权功能
3:加密功能
4:会话管理
5:缓存支持
6:rememberMe功能.....
不同点:
优点:
1:Spring Security基于Srpring开发,项目中如果使用Spring作为基础,配合Spring Security做权限更加方便,而Shiro需要和Spring进行整合开发
2:Spring Security功能比Shiro更加丰富些,例如安全防护
3:Spring Security社区资源比Shiro丰富
缺点:
1:Shiro的配置和使用比较简单,Spring Security上手复杂
2:Shiro依赖性低,不需要任何框架和容器,可以独立运行,而Spring Security依赖于Spring容器,一般来说,常见的安全管理技术栈的组合是这样的: SSM + Shiro • Spring Boot/Spring Cloud + Spring Security
1.3 Spring Security模块
- 核心模块 - spring -security-core.jar:包含核心验证和访问控制类和接口,远程支持的基本配置API,是基本模块
- 远程调用 - spring-security-remoting.jar:提供与 Spring Remoting 集成
- 网页 - spring-security-web.jar:包括网站安全的模块,提供网站认证服务和基于URL访问控制
- 配置 - spring-security-config.jar:包含安全命令空间解析代码,若使用XML进行配置则需要
- LDAP - spring-security-ldap.jar:LDAP 验证和配置,若需要LDAP验证和管理LDAP用户实体
- ACL访问控制表 - spring-security-acl.jar:ACL(Access Control List)专门领域对象的实现
- CAS - spring-security-cas.jar:CAS(Central Authentication Service)客户端继承,若想用CAS的 SSO服务器网页验证
- OpenID - spring-security-openid.jar:OpenID网页验证支持
- Test - spring-security-test.jar:支持Spring Security的测试
2 Spring Security使用
2.1 用户身份认证
快速开始
废话到此为止,下面正式开始
创建项目 - 引入依赖 - 编写controller - 运行。。。
(依赖引入如下:)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
上面是创建服务基本教程,下面正式开始。。。
编写controller
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/demo")
public String demo() {
return "spring security demo";
}
}
这里可以看到我们项目运行起来了,
在浏览器上输入:http://localhost:8080/login说明我们项目运行成功,注意:(Spring Security默认用户名是User,密码是上图运行项目时控制台日志生成的 password :3c532ef0-6ce4-463c-9079-99a4ac302e2c)如下图:
登录认证服务器(Authentication) 成功之后如下:
到这里说明我们基本的Spring Security已经成功认证(Authentication),下面我将介绍我们自定义登录模式
2.2设置用户名密码
基于application.yaml
spring:
# Spring Security 配置项,对应 SecurityProperties 配置类
security:
# 配置默认的 InMemoryUserDetailsManager 的用户账号与密码。
user:
name: chengsw # 账号
password: 123456 # 密码
roles: AMIND # 拥有角色
原理:
默认情况下,UserDetailsServiceAutoConfiguration自动化配置类,会创建一个内存级别的 InMemoryUserDetailsManager对象,提供认证的用户信息。
- 添加 spring.security.user 配置项,UserDetailsServiceAutoConfiguration 会基于配置 的信息在内存中创建一个用户User。
- 未添加 spring.security.user 配置项,UserDetailsServiceAutoConfiguration 会自动在 内存中创建一个用户名为 user,密码为 UUID 随机的用户 User。
基于UserDetailsService接口 我们创建service目录,定义一个UserDetailsServiceImpl类去实现UserDetailsService
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
UserDetails userDetails = User.withUsername("chengsw")
.password("{noop}123456").authorities("admin").build();
return userDetails;
}
}
Spring security5中新增加了加密方式,并把原有的spring security的密码存储格式改了,修改后的密码 存储格式为:
{id}encodedPassword
如果密码不指定{id}会抛异常:
支持的加密方式可以通过PasswordEncoderFactories查看
也可以通过增加PasswordEncoder配置指定加密方式,我们创建一个config的目录,再创建一个WebSecurityConfig类
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
spring security官方推荐的加密方式BCrypt完成登录认证
基于配置类WebSecurityConfigurerAdapter
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
思考: 是否需要添加@EnableWebSecurity注解?
springboot项目中如果引入的是spring-boot-starter-security依赖不需要添加
@EnableWebSecurity,可以参考自动配置类:
spring-boot-autoconfigure-2.3.5.RELEASE.jar!/META-INF/spring.factories 下 SecurityAutoConfiguration
如果引入的是spring-security-config和spring-security-web依赖,则需要添加 @EnableWebSecurity注解。
重写 #configure(AuthenticationManagerBuilder auth) 方法,实现 AuthenticationManager认 证管理器。
也可以重写WebSecurityConfigurerAdapter#userDetailsService()方法或者 WebSecurityConfigurerAdapter#userDetailsServiceBean(),并通过@Bean交给spring管理
@Configuration
public class WebSecurityConfig2 extends WebSecurityConfigurerAdapter {
@Bean
@Override
protected UserDetailsService userDetailsService()
{
return new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
String pw = passwordEncoder().encode("123456");
return new User("chengsw", pw,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,user"));
}
};
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
以上就是自定义认证登录,喜欢的朋友点个关注(一起学习,有写的不好的地方欢迎评论下方指正一下),下篇将讲实现Spring Security数据库认证,目前的规划是写完SSO,如果有时间还会单独写一篇通过源码解析Spring Security里面所使用到的设计模式,最后面将推出Spring Cloud Alibaba这套体系的前后端分离项目
这里先贴出Spring Security数据库认证需要的表
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(64) NOT NULL COMMENT '密码,加密存储', `phone` varchar(20) DEFAULT NULL COMMENT '注册手机号', `email` varchar(50) DEFAULT NULL COMMENT '注册邮箱', `created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`) USING BTREE,
UNIQUE KEY `phone` (`phone`) USING BTREE,
UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户表';
insert into `tb_user`(`id`,`username`,`password`,`phone`,`email`,`created`,`updated`) values (37,'fox','$2a$10$9ZhDOBp.sRKat4l14ygu/.LscxrMUcDAfeVOEPiYwbcRkoB09gCmi','158xxx xxxx','xxxxxxx@gmail.com','2019-04-04 23:21:27','2019-04-04 23:21:29');
CREATE TABLE `tb_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父角色', `name` varchar(64) NOT NULL COMMENT '角色名称', `enname` varchar(64) NOT NULL COMMENT '角色英文名称', `description` varchar(200) DEFAULT NULL COMMENT '备注', `created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='角色表'; insert into `tb_role`(`id`,`parent_id`,`name`,`enname`,`description`,`created`,`updated`) values
(37,0,'超级管理员','fox',NULL,'2019-04-04 23:22:03','2019-04-04 23:22:05');
CREATE TABLE `tb_user_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '用户 ID', `role_id` bigint(20) NOT NULL COMMENT '角色 ID', PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用户角色表'; insert into `tb_user_role`(`id`,`user_id`,`role_id`) values
(37,37,37);
CREATE TABLE `tb_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父权限', `name` varchar(64) NOT NULL COMMENT '权限名称', `enname` varchar(64) NOT NULL COMMENT '权限英文名称', `url` varchar(255) NOT NULL COMMENT '授权路径', `description` varchar(200) DEFAULT NULL COMMENT '备注', `created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8 COMMENT='权限表';
insert into `tb_permission`(`id`,`parent_id`,`name`,`enname`,`url`,`description`,`created`,` updated`) values
(37,0,'系统管理','System','/',NULL,'2019-04-04 23:22:54','2019-04-04 23:22:56'), (38,37,'用户管理','SystemUser','/users/',NULL,'2019-04-04 23:25:31','2019-04-04 23:25:33'),
(39,38,'查看用户','SystemUserView','',NULL,'2019-04-04 15:30:30','2019-04-04 15:30:43'),
(40,38,'新增用户','SystemUserInsert','',NULL,'2019-04-04 15:30:31','2019-04-04 15:30:44'),
(41,38,'编辑用户','SystemUserUpdate','',NULL,'2019-04-04 15:30:32','2019-04-04 15:30:45'),
(42,38,'删除用户','SystemUserDelete','',NULL,'2019-04-04 15:30:48','2019-04-04 15:30:45'),
(44,37,'内容管理','SystemContent','/contents/',NULL,'2019-04-06 18:23:58','2019- 04-06 18:24:00'), (45,44,'查看内容','SystemContentView','/contents/view/**',NULL,'2019-04-06 23:49:39','2019-04-06 23:49:41'), (46,44,'新增内容','SystemContentInsert','/contents/insert/**',NULL,'2019-04-06 23:51:00','2019-04-06 23:51:02'), (47,44,'编辑内容','SystemContentUpdate','/contents/update/**',NULL,'2019-04-06 23:51:04','2019-04-06 23:51:06'), (48,44,'删除内容','SystemContentDelete','/contents/delete/**',NULL,'2019-04-06 23:51:08','2019-04-06 23:51:10');
CREATE TABLE `tb_role_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) NOT NULL COMMENT '角色 ID', `permission_id` bigint(20) NOT NULL COMMENT '权限 ID', PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8 COMMENT='角色权限表'; insert into `tb_role_permission`(`id`,`role_id`,`permission_id`) values (37,37,37),
(38,37,38),
(39,37,39),
(40,37,40),
(41,37,41),
(42,37,42),
(43,37,44),
(44,37,45),
(45,37,46),
(46,37,47),
(47,37,48);
SpringSecurity原理剖析及其实战(二)
Spring Security整合数据库认证服务器
Spring security整合数据库认证本人这边所使用的技术栈如下:
技术 | 描述 |
mybatis |
持久层 |
mybatis-plus |
MyBatis (opens new window) 的增强工具 |
hutool |
java工具类库 |
lombok |
一种 Java™ 实用工具 |
EasyCode |
代码生成器 |
后面的整合oauth2、jwt中也会大量以上技术,包括最后整套Spring Cloud Alibaba - vue前后端分离体系
废话到这,下面正式开始
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.15</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
整体目录如下,
我这边使用的是EasyCode代码生成器,也可自己去配置mybatis的代码生成器,如需使用EasyCode代码生成器,Plugins 搜索easycode下载重启idea即可
这里分享一下我自定义的模版,只需点击导入模版输入:171c65ee922df8a54fb81abdf018761f,即可使用,如过期评论区下方留言即可,后面会逐步完善模版
EasyCode使用方式如下图,选择服务,生成路径以后需要生成的文件
- 2.application.yaml的连接池配置如下(数据库表已经在上一章最后面贴出来了)
server:
port: 8881
servlet:
context-path: /
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/hejinwen?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: root
hikari:
minimum-idle: 5
idle-timeout: 600000
maximum-pool-size: 10
auto-commit: true
pool-name: MyHikariCP
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
- 3.dao/TbPermissionDao.java
package com.csw.jdbc.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.csw.jdbc.entity.TbPermission;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface TbPermissionDao extends BaseMapper<TbPermission> {
List<TbPermission> selectByUserId(Long userId);
}
mapper/TbPermissionMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.csw.jdbc.dao.TbPermissionDao">
<select id="selectByUserId" resultType="com.csw.jdbc.entity.TbPermission">
select p.* from tb_user as u
LEFT JOIN tb_user_role ur on u.id = ur.user_id
LEFT JOIN tb_role as r on r.id = ur.role_id
LEFT JOIN tb_role_permission as p on r.id = p.permission_id
WHERE u.id = #{userId}
</select>
</mapper>
如下图,主要代码是selectByUserId()方法,其他是EasyCo de生成的方法,后面会逐步完善EasyCode模版:
service/TbPermissionService.java
package com.csw.jdbc.service;
import com.csw.jdbc.entity.TbPermission;
import java.util.List;
public interface TbPermissionService {
List<TbPermission> selectByUserId(Long userId);
}
service/impl/TbPermissionServiceImpl.java
package com.csw.jdbc.service.impl;
import com.csw.jdbc.dao.TbUserDao;
import com.csw.jdbc.entity.TbPermission;
import com.csw.jdbc.dao.TbPermissionDao;
import com.csw.jdbc.service.TbPermissionService;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
@AllArgsConstructor
public class TbPermissionServiceImpl implements TbPermissionService {
private final TbPermissionDao tbPermissionDao;
@Override
public List<TbPermission> selectByUserId(Long userId) {
return tbPermissionDao.selectByUserId(userId);
}
}
dao/TbUserDao.java
package com.csw.jdbc.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.csw.jdbc.entity.TbUser;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface TbUserDao extends BaseMapper<TbUser> {
}
service/TbUserService.java
package com.csw.jdbc.service;
import com.csw.jdbc.entity.TbUser;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import java.util.List;
public interface TbUserService {
TbUser getByUsername(String username);
}
service/impl/TbUserServiceImpl.java(核心代码)
package com.csw.jdbc.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.csw.jdbc.dao.TbPermissionDao;
import com.csw.jdbc.entity.TbPermission;
import com.csw.jdbc.entity.TbUser;
import com.csw.jdbc.dao.TbUserDao;
import com.csw.jdbc.service.TbUserService;
import lombok.AllArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
@Transactional
@AllArgsConstructor
public class TbUserServiceImpl implements TbUserService,UserDetailsService{
private final TbUserDao tbUserDao;
private final TbPermissionDao tbPermissionDao;
@Override
public TbUser getByUsername(String username) {
QueryWrapper<TbUser> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
return this.tbUserDao.selectOne(wrapper);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
TbUser user = getByUsername(username);
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
if(user != null) {
List<TbPermission> tbPermissions = tbPermissionDao.selectByUserId(user.getId());
tbPermissions.forEach(permissions -> {
if(permissions != null && !StrUtil.isEmpty(permissions.getEnname())) {
SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permissions.getEnname());
authorities.add(grantedAuthority);
}
});
return new org.springframework.security.core.userdetails.User(
user.getUsername(),user.getPassword(),authorities);
}else {
throw new UsernameNotFoundException("用户名不存在");
}
}
}
entity/TbUser.java
package com.csw.jdbc.entity;
import java.util.Date;
import java.io.Serializable;
import lombok.*;
@Data
public class TbUser implements Serializable {
private static final long serialVersionUID = 734210779083900189L;
private Long id;
private String username;
private String password;
private String phone;
private String email;
private Date created;
private Date updated;
}
entity/TbRole.java
package com.csw.jdbc.entity;
import java.util.Date;
import java.io.Serializable;
import lombok.*;
@Data
public class TbRole implements Serializable {
private static final long serialVersionUID = 768391666783014299L;
private Long id;
private Long parentId;
private String name;
private String enname;
private String description;
private Date created;
private Date updated;
}
entity/TbPermission.java
package com.csw.jdbc.entity;
import java.util.Date;
import java.io.Serializable;
import lombok.*;
@Data
public class TbPermission implements Serializable {
private static final long serialVersionUID = -91733717429724780L;
private Long id;
private Long parentId;
private String name;
private String enname;
private String url;
private String description;
private Date created;
private Date updated;
}
以上代码都可以使用EasyCode或者其他代码生成器生成的,核心代码只有TbUserServiceImpl.java里面的…
config/WebSecurityConfig.java
package com.csw.jdbc.config;
import com.csw.jdbc.service.impl.TbUserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private TbUserServiceImpl userServiceImpl;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceImpl);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
到这里注意,我已经在TbUserServiceImpl去实现了UserDetailsService接口的loadUserByUsername方法
整体代码已上传至(https://gitee.com/JAVA8888/spring-security.git),顺便在这里打个广告,推荐一个不错的SpringCloud Alibaba + vue项目(https://gitee.com/youlaitech/youlai-mall)
我们试下登录后的效果:
debug登录认证效果如下图:
SpringSecurity原理剖析及其实战(三)
1、自定义登录页面
默认登录页面通过DefaultLoginPageGeneratingFilter#generateLoginPageHtml生成
默认登录页面通过<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
用户名:<input type="text" name="username"/><br/>
密码: <input type="password" name="password"/><br/>
<input type="submit" value="提交"/>
</form>
</body>
</html>
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")
.defaultSuccessUrl("/admin/index")
.and().authorizeRequests()
.antMatchers("/user/login","/login.html")
.permitAll() .anyRequest().authenticated()
.and().csrf().disable();
}
测试
访问/admin/demo直接返回结果,不用认证
访问/admin/index跳转到自定义登录界面
常见问题:
原因是登录只接受Post请求 如下,通过successForwardUrl和failureForwardUrl设置登录成功和失败后的跳转页面
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")
.successForwardUrl("/main")
.failureForwardUrl("/toerror")
.and().authorizeRequests()
.antMatchers("/user/login","/login.html","/error.html").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
controller.java
@Controller
public class LoginController {
@RequestMapping("/main")
public String main() {
return "redirect:/main.html";
}
@RequestMapping("/toerror")
public String error() {
return "redirect:/error.html";
}
}
自定义用户名和密码参数名
当进行登录时会执行 UsernamePasswordAuthenticationFilter 过滤器
- usernamePasrameter :账户参数名
- passwordParameter :密码参数名
- postOnly=true :默认情况下只允许POST请求
2、过滤器链模式与责任链模式
过滤器链模式又称标准模式,这种模式主要使用不同标准来过滤一组对象。
过滤的过程便是一个层层筛选的过程,因此过滤器模式属于结构型设计模式的一种。
责任链模式顾名思义就是创建一个链条,经过这个链条处理的所有对象和数据分别进行依次加工,每个环节负责处理不同的业务,环节间彼此解耦,同时可以复用。
通信行业如移动会有很多营销活动,而这些营销活动的对象是有要求的,有的需要判断在网时长,有的需要有最低套餐要求等;
1.中国移动客户是目标角色;
2.它不同营销活动的要求是过滤器角色;
3.登录认证服务器需要经历层层过滤校验
过滤器链模式属于结构型设计模式
责任链模式属于行为型设计模式
结构型模式的目的是通过组合类或对象产生更大结构以适应更高层次的逻辑需求
从实现代码过滤上来看,过滤器模式更像是为了分组group by,而责任链模式是让多个对象都有可能接受请求,将这些对象连接成一条链,并且沿着这条链传递请求。
- 2.2 AbstractAuthenticationProcessingFilter的认证过滤
3、Security Session
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring
security提供会话管理,认证通过后将身份信息放入Security
ContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。
这里做一个小复习,我们知道客户端请求服务器端是用http(超文本传输协议)请求,而http请求是无状态的,所以我们的状态是存在服务器(session)或者浏览器(cookie)
1.cookie数据存放在客户的浏览器上,session数据放在服务区上
2.cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗考虑到安全应当使用session。
3.设置cookie时间可以使cookie过期。但是使用session-destory(),我们将会销毁会话。
4.session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能考虑到减轻服务器性能方面,应当使用cookie。
5.单个cookie保存到数据不能超过4k,很多浏览器都限制一个站点最多保存20个cookie。(session对象没有对存储的数据量的限制,其中可以保存更为复杂的数据类型)
1、session很容易失效,用户体验很差;
2、虽然cookie不安全,但是可以加密 ;
3、cookie也分为永久和暂时存在的;
4、浏览器 有禁止cookie功能 ,但一般用户都不会设置;
5、一定要设置失效时间,要不然浏览器关闭就消失了;
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/demo")
public String demo() {
String username = getUsername();
return username + "spring security demo";
}
private String getUsername() {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
if (!authentication.isAuthenticated()) {
return null;
}
Object principal = authentication.getPrincipal();
String username = null;
if(principal instanceof UserDetails) {
username = ((UserDetails)principal).getUsername();
} else {
username = principal.toString();
}
return username;
}
}
-
- 3.3 会话控制
我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互:
机制 | 描述 |
always |
如果session不存在总是需要创建 |
ifRequired |
如果需要就创建一个session(默认)登录时 |
never |
Spring Security 将不会创建session,但是如果应用中其他地方创建了session,那 么Spring Security将会使用它 |
stateless |
Spring Security将绝对不会创建session,也不使用session。并且它会暗示不使用 cookie,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API 及其无状态认证机制。 |
http.sessionManagement()
.invalidSessionUrl("/session/invalid");
默认情况下,Spring Security会为每个登录成功的用户会新建一个Session,就是ifRequired 。在执行认 证过程之前,spring security将运行SecurityContextPersistenceFilter过滤器负责存储安全请求上下 文,上下文根据策略进行存储,默认为HttpSessionSecurityContextRepository ,其使用http session 作为存储器。
可以在sevlet容器中设置Session的超时时间,如下设置Session有效期为600s; spring boot 配置文件:
注意:session最低60s,参考源码TomcatServletWebServerFactory#configureSession:
session超时之后,可以通过Spring Security 设置跳转的路径。
@RestController
@RequestMapping("/session")
public class SessionController {
@GetMapping("/invalid")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public String sessionInvalid() {
return "session失效";
}
}
用户在这个手机登录后,他又在另一个手机登录相同账户,对于之前登录的账户 是否需要被挤兑,或者说在第二次登录时限制它登录,更或者像腾讯视频VIP账号一样,最多只能五个人同时登录,第六个人限制登录。
-
-
- maximumSession:最大会话数量,设置为1表示一个用户只能有一个会话
-
-
- expiredSessionStrategy:会话过期策略
http.sessionManagement()
.invalidSessionUrl("/session/invalid")
.maximumSessions(1)
.expiredSessionStrategy(new MyExpiredSessionStrategy());
public class MyExpiredSessionStrategy implements
SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event)
throws IOException, ServletException {
} }
HttpServletResponse response = event.getResponse(); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("您已被挤兑下线!");
测试
- 使用chrome浏览器,先登录,再访问http://localhost:8080/admin/index
- 使用ie浏览器,再登录,再访问http://localhost:8080/admin/index
- 使用chrome浏览器,重新访问http://localhost:8080/admin/index,会执行
expiredSessionStrategy,页面上显示 “您已被挤兑下线!”
阻止用户第二次登录
sessionManagement也可以配置 maxSessionsPreventsLogin:boolean值,当达到 maximumSessions设置的最大会话个数时阻止登录。
http.sessionManagement()
.invalidSessionUrl("/session/invalid")
.maximumSessions(1)
.expiredSessionStrategy(new MyExpiredSessionStrategy())
.maxSessionsPreventsLogin(true);
实际场景中一个服务会至少有两台服务器在提供服务,在服务器前面会有一个nginx做负载均衡,用户访 问nginx,nginx再决定去访问哪一台服务器。当一台服务宕机了之后,另一台服务器也可以继续提供服 务,保证服务不中断。如果我们将session保存在Web容器(比如tomcat)中,如果一个用户第一次访问被 分配到服务器1上面需要登录,当某些访问突然被分配到服务器二上,因为服务器二上没有用户在服务器 一上登录的会话session信息,服务器二还会再次让用户登录,用户已经登录了还让登录就感觉不正常 了。解决这个问题的思路是用户登录的会话信息不能再保存到Web服务器中,而是保存到一个单独的库 (redis、mongodb、mysql等)中,所有服务器都访问同一个库,都从同一个库来获取用户的session信 息,如用户在服务器一上登录,将会话信息保存到库中,用户的下次请求被分配到服务器二,服务器二 从库中检查session是否已经存在,如果存在就不用再登录了,可以直接访问服务了。
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
修改application.yaml
spring:
session:
store-type: redis
redis:
host: localhost
port: 6379
server:
port: 8080
servlet:
session:
timeout: 600
测试
启动两个服务8080,8081 ,其中一个登录后访问http://localhost:8080/admin/index,另外一个不需
要登录就可以访问
安全会话cookie 我们可以使用httpOnly和secure标签来保护我们的会话cookie:
- httpOnly:如果为true,那么浏览器脚本将无法访问cookie
- secure:如果为true,则cookie将仅通过HTTPS连接发送
spring boot配置文件:
server.servlet.session.cookie.http-only=true server.servlet.session.cookie.secure=true
4、RememberMe实现
Spring Security 中 Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,
取值为true。Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问。
@Autowired
public DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(3600)
.userDetailsService(userServiceImpl);
}
public PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
create table persistent_logins (
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null
)
在客户端登录页面中添加 remember-me 的复选框,只要用户勾选了复选框下次就不需要进行登录了。
<form action="/user/login" method="post">
用户名:<input type="text" name="username"/><br/>
密码: <input type="password" name="password"/><br/>
<input type="checkbox" name="remember-me" value="true"/><br/>
<input type="submit" value="提交"/>
</form>
5、退出登录
Spring security默认实现了logout退出,用户只需要向 Spring Security 项目中发送 /logout 退出请求即 可。
默认的退出 url 为 /logout ,退出成功后跳转到 /login?logout 。
自定义退出逻辑
如果不希望使用默认值,可以通过下面的方法进行修改。
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html");
SecurityContextLogoutHandler
当退出操作出发时,将发生:
销毁HTTPSession 对象 清除认证状态
跳转到 /login.html
LogoutSuccessHandler
退出成功处理器,实现 LogoutSuccessHandler 接口 ,可以自定义退出成功处理逻辑。
6、CSRF
1、CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack” 或者Session Riding。 通过伪造用户请求访问受信任站点的非法请求访问。
2、跨域:只要网络协议,ip 地址,端口中任何一个不相同就是跨域请求。
3、客户端与服务进行交互时,由于 http 协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id 可能被第三方恶意劫 持,通过这个session id 向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不 到的事情。
防御CSRF攻击策略
从 Spring Security4开始CSRF防护默认开启,默认会拦截请求,进行CSRF处理。CSRF为了保证不是其 他第三方网站访问,要求访问时携带参数名为 _csrf 值为token(token 在服务端产生,在渲染请求页面时 埋入页面)的内容,如果token和服务端的token匹配成功,则正常访问。
修改login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
<input type="hidden" th:value="${_csrf.token}" name="_csrf"
th:if="${_csrf}"/>
用户名:<input type="text" name="username"/><br/> 密码: <input type="password" name="password"/><br/> <input type="submit" value="提交"/>
</form>
</body>
</html>
修改配置类
以上就是Spring Security的认证使用,下篇是认证服务器整体流程的梳理,喜欢的朋友点个关注,顺便推荐一个不错的Spring Cloud Alibaba + vue的项目,https://gitee.com/youlaitech/youlai-mall
SpringSecurity原理剖析及其实战(四)
1、认证原理
Spring Security是如何完成身份认证的?
- 用户名和密码被过滤器获取到,封装成Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
- AuthenticationManager身份管理器负责验证这个Authentication
- 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例
- SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过 SecurityContextHolder.getContext().setAuthentication(…) 方法中,设置到其中。
public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new
InputStreamReader(System.in));
while (true) {
System.out.println("Please enter your username:");
String name = in.readLine();
System.out.println("Please enter your password:");
String password = in.readLine();
try {
Authentication request = new
UsernamePasswordAuthenticationToken(name, password);
Authentication result = am.authenticate(request);
} catch (AuthenticationException e) {
System.out.println("Authentication failed: " + e.getMessage());
} }
System.out.println("Successfully authenticated. Security context
contains: " +
SecurityContextHolder.getContext().getAuthentication());
} }
class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new
ArrayList<GrantedAuthority>();
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public Authentication authenticate(Authentication auth) throws
AuthenticationException {
if (auth.getName().equals(auth.getCredentials())) {
return new UsernamePasswordAuthenticationToken(auth.getName(),
auth.getCredentials(), AUTHORITIES);
throw new BadCredentialsException("Bad Credentials");
} }
测试
2、认证流程
3、相关接口
AuthenticationManager
认证管理器,用于处理一个认证请求,提供了认证方法的入口
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
ProviderManager
ProviderManager是 AuthenticationManager 的一个实现类,提供了基本的认证逻辑和方法;它包含 了一个List属性,通过 AuthenticationProvider 接口来扩展出多种认证方式,实际上这是委托者模式的 应用(Delegate)。
委托模式概念
一个对象接收到了请求,但是自己不处理,交给另外的对象处理,就是委托模式,例如 老板接到了活,然后把活转手给了工人去做。
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
在Spring Security中,提交的用户名和密码,被封装成UsernamePasswordAuthenticationToken,而 根据用户名加载用户的任务则是交给了UserDetailsService,在DaoAuthenticationProvider中,对应的 方法便是retrieveUser,返回一个UserDetails。
Authentication
Authentication在spring security中是最高级别的身份/认证的抽象,由这个顶级接口,我们可以得到用 户拥有的权限信息列表,密码,用户细节信息,用户身份信息,认证信息。
UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken 实现了 Authentication 主要是将用户输入的用户名和密 码进行封装,并供给AuthenticationManager 进行验证;验证完成以后将返回一个认证成功的Authentication 对象
public interface Authentication extends Principal, Serializable {
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_ADMIN")返回字符串权 限集合
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
访问者的ip地址和sessionId的值。
Object getDetails();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws
IllegalArgumentException;
}
SecurityContextHolder
用于存储安全上下文(security context)的信息, SecurityContextHolder 默认使用 ThreadLocal 策略来存储认证信息。
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
UserDetailsService
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException;
}
Spring Security内置了两种 UserDetailsManager实现
实际项目中,我们更多采用调用
AuthenticationManagerBuilder.userDetailsService(userDetailsService) 方法,使用自定义 实现的 UserDetailsService实现类,更加灵活且自由的实现认证的用户信息的读取。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new UserDetailsServiceImpl());
}
UserDetails
用户信息核心接口,默认实现类org.springframework.security.core.userdetails.User
PasswordEncoder
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
}
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器
@Test
public void test(){
String passwd = BCrypt.hashpw("123",BCrypt.gensalt());
System.out.println(passwd);
boolean checkpw = BCrypt.checkpw("123", passwd);
System.out.println(checkpw);
}
SpringSecurity原理剖析及其实战(五)
1、用户授权(访问控制)
-
- 什么是授权?
授权的方式常分为两种,web授权和方法授权,web授权是通过url拦截进行授权,方法授权是通过方法拦截进行授权。他们都会调用accessDecisionManager进行授权决策,若为web授权则拦截器为FilterSecurityInterceptor;若为方法授权则拦截器为MethodSecurityInterceptor。如果同时通过web授权和方法授权则先执行web授权,再执行方法授权,最后决策通过,则允许访问资源,否则将禁止访问。
-
- web授权
Spring Security可以通过http.authorizeRequests() 对web请求进行授权保护 Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。
http.authorizeRequests()
.antMatchers("/user/login","/login.html").permitAll() .anyRequest().authenticated();
-
- 访问控制的url(这种现实场景比较少用)
在配置类中http.authorizeRequests() 主要是对url进行控制。配置顺序会影响之后授权的效果,越是具 体的应该放在前面,越是笼统的应该放到后面。
-
-
- anyRequest()
表示匹配所有的请求。一般情况下此方法都会使用,设置全部内容都需要进行认证,会放在最后。
-
-
- antMatchers()
参数是不定向参数,每个参数是一个 ant 表达式,用于匹配 URL规则。
ANT通配符有三种:
通配符 | 说明 |
? |
匹配任何单字符 |
* |
匹配0或者任意数量的字符 |
** |
匹配0或者更多的目录 |
-
-
- regexMatchers()
使用正则表达式进行匹配。
.regexMatchers( ".+[.]js").permitAll()
无论是 antMatchers() 还是 regexMatchers() 都具有两个参数的方法,其中第一个参数都是 HttpMethod ,表示请求方式,当设置了 HttpMethod 后表示只有设定的特定的请求方式才执行对应的 权限设置。
-
-
- mvcMatchers()
适用于配置了 servletPath 的情况。 servletPath 就是所有的 URL 的统一前缀。在 SpringBoot 整合 SpringMVC 的项目中可以在application.properties 中添加下面内容设置 ServletPath。
-
-
- RequestMatcher接口
RequestMatcher 是 Spring Security Web 的一个概念模型接口,用于抽象建模对
HttpServletRequest 请求的匹配器这一概念。 Spring Security 内置提供了一些 RequestMatcher
实现类:
内置的访问控制
-
- 【常用】#permitAll() 方法,所有用户可访问。
-
- 【常用】#denyAll() 方法,所有用户不可访问。
-
- 【常用】 #authenticated() 方法,登录用户可访问。
-
- #anonymous() 方法,无需登录,即匿名用户可访问。
-
- #rememberMe() 方法,通过 remember me登录的用户可访问。
-
- #fullyAuthenticated() 方法,非 remember me 登录的用户可访问。
-
- #hasIpAddress(String ipaddressExpression) 方法,来自指定 IP 表达式的用户可访问。
-
- 【常用】 #hasRole(String role) 方法, 拥有指定角色的用户可访问,角色将被增加 “ROLE_” 前缀。
-
- 【常用】 #hasAnyRole(String… roles) 方法,拥有指定任一角色的用户可访问。
-
- 【常用】 方法,拥有指定权限( )的用户可访 问。
-
- 【常用】 #hasAuthority(String… authorities) 方法,拥有指定任一权限( authority )的用 户可访问。
-
- 【最牛】 #access(String attribute) 方法,当 Spring EL 表达式的执行结果为 true 时,可以 访问。
2、基于权限的访问控制
除了之前讲解的内置权限控制。Spring Security 中还支持很多其他权限控制。这些方法一般都用于用户 已经被认证后,判断用户是否具有特定的要求。
-
-
- hasAuthority(String)
判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建 User 对象时指定的。权限名称 大小写敏感
return new User("fox", pw, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,user"));
在配置类中通过 hasAuthority(“admin”)设置具有 admin 权限时才能访问。
.antMatchers("/admin/demo").hasAuthority("admin")
-
-
- hasAnyAuthority(String …)
如果用户具备给定权限中某一个,就允许访问。
.antMatchers("/admin/demo").hasAnyAuthority("admin","System")
-
-
-
- hasRole(String)
如果用户具备给定角色就允许访问,否则出现 403。参数取值来源于自定义登录逻辑 UserDetailsService 实现类中创建 User 对象时给 User 赋予的授权。 在给用户赋予角色时角色需要以:
ROLE_ 开头 ,后面添加角色名称。例如:ROLE_admin 其中 admin是角 色名, ROLE_ 是固定的字符开 头。
return new User("fox", pw, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin,user"));
使用 hasRole()时参数也只写admin 即可,否则启动报错。
.antMatchers("/admin/demo").hasRole("admin")
-
-
- hasAnyRole(String …)
如果用户具备给定角色的任意一个,就允许被访问 。
-
-
- hasIpAddress(String)
如果请求是指定的 IP 就运行访问。 可以通过 request.getRemoteAddr() 获取 ip 地址。需要注意的是在 本机进行测试时 localhost 和 127.0.0.1 输出的 ip地址是不一样的。
.antMatchers("/admin/demo").hasIpAddress("127.0.0.1")
-
- 自定义403处理方案
使用 Spring Security 时经常会看见 403(无权限)。Spring Security 支持自定义权限受限处理,需要
实现 AccessDeniedHandler接口
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException,
ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setHeader("Content-Type", "application/json;charset=utf-8"); PrintWriter out = response.getWriter(); out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}"); out.flush();
out.close();
} }
在配置类中设置访问受限后交个MyAccessDeniedHandler处理
http.exceptionHandling()
.accessDeniedHandler(new MyAccessDeniedHandler());
3、基于表达式的访问控制
-
- access(表达式)
之前学习的登录用户权限判断实际上底层实现都是调用access(表达式)
相关链接
4、方法授权
Spring Security在方法的权限控制上支持三种类型的注解,JSR-250注解、@Secured注解和支持表达式的注解。这三种注解默认都是没有启用的,需要通过@EnableGlobalMethodSecurity来进行启用。
这些注解可以写到 Service 接口或方法上,也可以写到 Controller或 Controller 的方法上。通常情况下都是写在控制器方法上的,控制接口URL是否允许被访问。
表示访问对应方法时所应该具有的角色。其可以标注在类上,也可以标注在方法上,当标注在类上时表
示其中所有方法的执行都需要对应的角色,当标注在方法上表示执行该方法时所需要的角色,当方法和
类上都使用了@RolesAllowed进行标注,则方法上的@RolesAllowed将覆盖类上的@RolesAllowed,即
方法上@RolesAllowed将对当前方法起作用。@RolesAllowed的值是由角色名称组成的数组。
表示允许所有的角色进行访问,也就是说不进行权限控制。@PermitAll可以标注在方法上也可以标注在类上,当标注在方法上时则只对对应方法不进行权限控制,而标注在类上时表示对类里面所有的方法都不进行权限控制。
(1)当@PermitAll标注在类上,而@RolesAllowed标注在方法上时则按照
@RolesAllowed将覆盖@PermitAll,即需要@RolesAllowed对应的角色才能访问。
(2)当@RolesAllowed标注在类上,而@PermitAll标注在方法上时则对应的方法也是不进行权限控制 的。
(3)当在类和方法上同时使用了@PermitAll和@RolesAllowed时先定义的将发生作用(这个没多大的实际意义,实际应用中不会有这样的定义)。
是和PermitAll相反的,表示无论什么角色都不能访问。@DenyAll只能定义在方法上。你可能会有疑问
使用@DenyAll标注的方法无论拥有什么权限都不能访问,那还定义它干啥呢?使用@DenyAll定义的方法只是在我们的权限控制中不能访问,脱离了权限控制还是可以访问的。
开启注解 在启动类或者在配置类上添加 @EnableGlobalMethodSecurity(jsr250Enabled = true)
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
在controller方法上添加@RolesAllowed注解
@RolesAllowed({"ROLE_USER", "ROLE_ADMIN"})
@GetMapping("/demo")
public String demo() {
return "spring security demo";
}
修改配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html")
.and().authorizeRequests()
.and().csrf().disable();
}
@Secured是由Spring Security定义的用来支持方法权限控制的注解。它的使用也是需要启用对应的支
持才会生效的。@Secured 是专门用于判断是否具有角色的,能写在方法或类上。参数要以 ROLE_开 头。
开启注解 在启动类或者在配置类上添加 @EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
在controller方法上添加@Secured 注解
@Secured("ROLE_ADMIN")
@GetMapping("/demo")
public String demo() {
return "spring security demo";
}
Spring Security中定义了四个支持使用表达式的注解,分别是@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter。其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
-
-
- 使用@PreAuthorize和@PostAuthorize进行访问控制
@PreAuthorize可以用来控制一个方法是否能够被调用,执行之前先判断权限,大多情况下都是使用这个注解。
@PreAuthorize("#id<10")
@RequestMapping("/findById")
public User findById(long id) {
User user = new User();
user.setId(id);
return user;
}
public User findByName(String username) {
User user = new User();
user.setUsername(username);
return user;
}
public User add(User user) {
return user;
}
@PostAuthorize可以在方法调用完之后进行权限检查
@PostAuthorize("returnObject.id%2==0")
public User find(int id) {
User user = new User();
user.setId(id);
return user;
}
-
-
- 使用@PreFilter和@PostFilter进行过滤
@PostFilter("filterObject.id%2==0")
public List<User> findAll() {
List<User> userList = new ArrayList<User>();
User user;
for (int i=0; i<10; i++) {user = new User();
user.setId(i);
userList.add(user);
}
return userList;
}
@PreFilter(filterTarget="ids", value="filterObject%2==0")
public void delete(List<Integer> ids, List<String> usernames) {
}
5 、授权原理
重写 #configure(HttpSecurity http) 方法,主要配置 URL 的权限控制
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/test/echo").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.and()
.logoutUrl("/logout")
.permitAll();
}
调用 HttpSecurity#authorizeRequests() 方法,开始配置 URL 的权限控制。
修改 WebSecurityConfig配置类,增加 @EnableGlobalMethodSecurity 注解,开启对 Spring Security 注解的方法,进行权限验证。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@RestController
@RequestMapping("/demo")
public class DemoController {
@PermitAll
@GetMapping("/echo")
public String demo() {
return "示例返回"; }
@GetMapping("/home")
public String home() {
return "我是首页"; }
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/admin")
public String admin() {
return "我是管理员"; }
@PreAuthorize("hasRole('ROLE_NORMAL')")
@GetMapping("/normal")
public String normal() {
return "我是普通用户"; }
}
6、授权流程
-
- 基于Filter
- 拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的 FilterSecurityInterceptor 的子 类拦截。
- 获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类 DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限Collection 。 SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访 问规则
- 最后,FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则 允许访问资 源,否则将禁止访问。
public Object invoke(MethodInvocation mi) throws Throwable {
InterceptorStatusToken token = super.beforeInvocation(mi);
Object result;
try {
result = mi.proceed();
}
finally {
super.finallyInvocation(token);
}
return super.afterInvocation(token, result);
}
-
- 相关接口
AccessDecisionManager
AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源。 AccessDecisionManager 中包含的一系列AccessDecisionVoter将会被用来对Authentication是否有权访问受保护对象进行投票, AccessDecisionManager根据投票结果,做出最终决策 。
public interface AccessDecisionManager {
================================================================================
========================
void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws
AccessDeniedException,
InsufficientAuthenticationException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
}
AffirmativeBased的逻辑是:
(1)只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;
(2)如果全部弃权也表示通过;
(3)如果没有一个人投赞成票,但是有人投反对票,则将抛AccessDeniedException。 Spring security默认使用的是AffirmativeBased。
ConsensusBased的逻辑是:
(1)如果赞成票多于反对票则表示通过。
(2)反过来,如果反对票多于赞成票则将抛出AccessDeniedException。
(3)如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions的值为true,则表 示通过,否则将抛出异常
AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认为true。
(4)如果所有的AccessDecisionVoter都弃权了,则将视参allowIfAllAbstainDecisions的值而定,如果该值为true则表示通过,否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false。
UnanimousBased的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部 传递给AccessDecisionVoter进行投票,而UnanimousBased会一次只传递一个ConfigAttribute给AccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来 的ConfigAttribute中有一个能够匹配则投赞成票,但是放到UnanimousBased中其投票结果就不一定 是赞成了。
UnanimousBased的逻辑具体来说是这样的:
(1)如果受保护对象配置的某一个 ConfigAttribute被任意的AccessDecisionVoter反对了,则将抛出 AccessDeniedException。
(2)如 果没有反对票,但是有赞成票,则表示通过。
(3)如果全部弃权了,则将视参数allowIfAllAbstainDecisions的值而定,true则通过,false则抛出 AccessDeniedException。
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
}
-
-
- MethodSecurityInterceptor
Spring Security提供了两类AbstractSecurityInterceptor,基于AOP Alliance的 MethodSecurityInterceptor,和基于Aspectj继承自MethodSecurityInterceptor的 AspectJMethodSecurityInterceptor
public Object invoke(MethodInvocation mi) throws Throwable {
InterceptorStatusToken token = super.beforeInvocation(mi);
Object result;
try {
result = mi.proceed();
}
finally {
super.finallyInvocation(token);
}
return super.afterInvocation(token, result);
}
到这里spring security算是完结了,下篇开始就是spring security oauth2的实战学习