SpringSecurity 安全框架详解
1.简介
先赘述一下身份认证和用户授权:
- 用户认证(
Authentication
):系统通过校验用户提供的用户名和密码来验证该用户是否为系统中的合法主体,即是否可以访问该系统; - 用户授权(
Authorization
):系统为用户分配不同的角色,以获取对应的权限,即验证该用户是否有权限执行该操作;
Web
应用的安全性包括用户认证和用户授权两个部分,而Spring Security
(以下简称Security
)基于Spring
框架,正好可以完整解决该问题。
它的真正强大之处在于它可以轻松扩展以满足自定义要求。
2.原理
Security
可以看做是由一组filter
过滤器链组成的权限认证。它的整个工作流程如下所示:
图中绿色认证方式是可以配置的,橘黄色和蓝色的位置不可更改:
-
FilterSecurityInterceptor
:最后的过滤器,它会决定当前的请求可不可以访问Controller
-
ExceptionTranslationFilter
:异常过滤器,接收到异常消息时会引导用户进行认证;
2.1 项目准备
我们使用Spring Boot
框架来集成。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.74</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
application.yml
配置
spring:
application:
name: securityjwt
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/cheetah?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: 123456
server:
port: 8080
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.itcheetah.securityjwt.entity
configuration:
map-underscore-to-camel-case: true
rsa:
key:
pubKeyFile: C:\Users\Desktop\jwt\id_key_rsa.pub
priKeyFile: C:\Users\Desktop\jwt\id_key_rsa
3.SQL
文件
/**
\* sys_user_info
**/*
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
*-- ----------------------------*
*-- Table structure for sys_user_info*
*-- ----------------------------*
DROP TABLE IF EXISTS `sys_user_info`;
CREATE TABLE `sys_user_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
*/**
\* product_info
**/*
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
*-- ----------------------------*
*-- Table structure for product_info*
*-- ----------------------------*
DROP TABLE IF EXISTS `product_info`;
CREATE TABLE `product_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`price` decimal(10, 4) NULL DEFAULT NULL,
`create_date` datetime(0) NULL DEFAULT NULL,
`update_date` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
2.2 jwt 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
引入之后启动项目,会有如图所示:
其中用户名为user
,密码为上图中的字符串。
2.3 有关Security类详解
2.3.1 SecurityConfig详解
//开启全局方法安全性*
@EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
*//认证失败处理类*
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
*//提供公钥私钥的配置类*
@Autowired
private RsaKeyProperties prop;
@Autowired
private UserInfoService userInfoService;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
*// CSRF禁用,因为不使用session*
.csrf().disable()
*// 认证失败处理类*
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
*// 基于token,所以不需要session*
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
*// 过滤请求*
.authorizeRequests()
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
*// 除上面外的所有请求全部需要鉴权认证*
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
*// 添加JWT filter*
httpSecurity.addFilter(new TokenLoginFilter(super.authenticationManager(), prop))
.addFilter(new TokenVerifyFilter(super.authenticationManager(), prop));
}
*//指定认证对象的来源*
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userInfoService)
*//从前端传递过来的密码就会被加密,所以从数据库*
*//查询到的密码必须是经过加密的,而这个过程都是*
*//在用户注册的时候进行加密的。*
.passwordEncoder(passwordEncoder());
}
*//密码加密*
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
「拦截规则」
-
anyRequest
:匹配所有请求路径 -
access
:SpringEl
表达式结果为true
时可以访问 -
anonymous
:匿名可以访问 -
denyAll
:用户不能访问 -
fullyAuthenticated
:用户完全认证可以访问(非remember-me
下自动登录) -
hasAnyAuthority
:如果有参数,参数表示权限,则其中任何一个权限可以访问 -
hasAnyRole
:如果有参数,参数表示角色,则其中任何一个角色可以访问 -
hasAuthority
:如果有参数,参数表示权限,则其权限可以访问 -
hasIpAddress
:如果有参数,参数表示IP
地址,如果用户IP
和参数匹配,则可以访问 -
hasRole
:如果有参数,参数表示角色,则其角色可以访问 -
permitAll
:用户可以任意访问 -
rememberMe
:允许通过remember-me
登录的用户访问 -
authenticated
:用户登录后可访问
2.3.2 认证失败处理类
/**
\* 返回未授权
\*/*
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
throws IOException {
int code = HttpStatus.UNAUTHORIZED;
String msg = "认证失败,无法访问系统资源,请先登陆";
ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
}
}
2.4 认证流程
2.4.1 自定义认证过滤器
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RsaKeyProperties prop;
public TokenLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
this.authenticationManager = authenticationManager;
this.prop = prop;
}
*/**
\* @author cheetah
\* @description 登陆验证
\* @date 2021/6/28 16:17
\* @Param [request, response]
\* @return org.springframework.security.core.Authentication
**/*
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
UserPojo sysUser = new ObjectMapper().readValue(request.getInputStream(), UserPojo.class);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());
return authenticationManager.authenticate(authRequest);
}catch (Exception e){
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map resultMap = new HashMap();
resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);
resultMap.put("msg", "用户名或密码错误!");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
}catch (Exception outEx){
outEx.printStackTrace();
}
throw new RuntimeException(e);
}
}
*/**
\* @author cheetah
\* @description 登陆成功回调
\* @date 2021/6/28 16:17
\* @Param [request, response, chain, authResult]
\* @return void
**/*
public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
UserPojo user = new UserPojo();
user.setUsername(authResult.getName());
user.setRoles((List<RolePojo>)authResult.getAuthorities());
*//通过私钥进行加密:token有效期一天*
String token = JwtUtils.generateTokenExpireInMinutes(user, prop.getPrivateKey(), 24 * 60);
response.addHeader("Authorization", "Bearer "+token);
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
Map resultMap = new HashMap();
resultMap.put("code", HttpServletResponse.SC_OK);
resultMap.put("msg", "认证通过!");
resultMap.put("token", token);
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
}catch (Exception outEx){
outEx.printStackTrace();
}
}
}
2.4.2 登录流程
Security
默认登录路径为/login
,当我们调用该接口时,会在登录的时候调方法loadUserByUsername(username)
该方法的具体实现类
如果用户名称不存在就直接抛出用户名或密码错误,如果用户名字存在,在把该用户对应的信息和权限用AdminUserDetails这个类接收
需要注意的是,这个接收用户信息的接受类需要实现SpringSecurity中的接口UserDetails
如果密码正确的话,就把对应的用户用户信息和用户权限用UsernamePasswordAuthenticationToken初始化,并且增加Security框架中的过滤器增加该权限setAuthentication。
ps:这个类UsernamePasswordAuthenticationToken在下文验证token时会出现。
如果Secruity框架都没啥问题了,这时候就掉用生成token的接口了
token = jwtTokenUtil.generateToken(userDetails);
接口大概的实现方法,就是拿用户名称是做Jwt的载体
2.4.3 这里顺便提一下Jwt
jwt简称json web token,他是由`.分割的三部分组成,这三部分依次是:
- 头部(Header)
- 负载(Payload)
- 签名(Signature)
Header
JWT的Header中存储了所使用的加密算法和Token类型
{
"alg" : "HS256",
"typ" : "JWT"
}
Payload
payload表示负载,也是一个JSON对象,JWT规定了7个官方字段供选用。
iss (issuer) : 签发人
exp (expiration time) : 过期时间
sub (subject) : 主题
aud (audience) : 受众
nbf (Not Before) : 生效时间
iat (Issued At) : 签发时间
jti (JWT ID) : 编号
Signature
Signature部分是对前两部分的签名,防止数据篡改。
首先,需要指定一一个密钥(secret) 。 这个密钥只有服务器才知道,不能泄露给用户。然后,使用Header里面指定的签名算法(默认是HMAC SHA256),按照下面的公式产生签名。
JWT优缺点:
JWT拥有基于Token的会话管理方式所拥有的一切优势,不依赖Cookie,使得其可以防止CSRF攻击,也能在禁用Cookie的浏览器环境中正常运行。
而JWT的最大优势是服务端不再需要存储Session,使得服务端认证鉴权业务可以方便扩展,避免存储Session所需要引入的Redis等组件,降低了系统架构复杂度。但这也是JWT最大的劣势,由于有效期存储在Token中,JWT Token-旦签发,就会在有效期内-直可用,无法在服务端废止,当用户进行登出操作,只能依赖客户端删除掉本地存储的JWT Token,如果需要禁用用户,单纯使用JWT就无法做到了。
2.5 整体流程
具体代码地址
https://github.com/hongjiatao/spring-boot-anyDemo
可以的话点赞三连,也是对我最大的支持。谢谢