第30篇-Spring-Security-7核心概念

【Kotlin + Spring Boot 4 从零到架构师】第 30 篇:Spring Security 7 核心概念

本系列定位:零基础入门,从 Kotlin 语法一路到 Spring Boot 4 高级架构(DDD + Modulith),适合 Java 开发者转型,也适合纯新手系统学习。


本篇你将学到

  • 认证(Authentication)与授权(Authorization)的区别
  • Spring Security 的 FilterChain 架构
  • SecurityFilterChain 配置(Lambda DSL)
  • UserDetailsServiceUserDetails
  • 权限表达式:hasRole / hasAuthority / permitAll

学完本篇,你将理解 Spring Security 的工作原理,并能为 mini-shop 配置基础的安全规则。


一、认证 vs 授权

这两个概念是安全领域的基石,初学者经常混淆。

1.1 定义

概念 英文 解决的问题 通俗说法
认证 Authentication 你是谁? 「你是谁?」→ 验证身份(登录)
授权 Authorization 你能做什么? 「你能干嘛?」→ 检查权限

1.2 生活类比

#mermaid-svg-fNitaMzgceGvcvLb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fNitaMzgceGvcvLb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fNitaMzgceGvcvLb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fNitaMzgceGvcvLb .error-icon{fill:#552222;}#mermaid-svg-fNitaMzgceGvcvLb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fNitaMzgceGvcvLb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fNitaMzgceGvcvLb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fNitaMzgceGvcvLb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fNitaMzgceGvcvLb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fNitaMzgceGvcvLb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fNitaMzgceGvcvLb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fNitaMzgceGvcvLb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fNitaMzgceGvcvLb .marker.cross{stroke:#333333;}#mermaid-svg-fNitaMzgceGvcvLb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fNitaMzgceGvcvLb p{margin:0;}#mermaid-svg-fNitaMzgceGvcvLb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fNitaMzgceGvcvLb .cluster-label text{fill:#333;}#mermaid-svg-fNitaMzgceGvcvLb .cluster-label span{color:#333;}#mermaid-svg-fNitaMzgceGvcvLb .cluster-label span p{background-color:transparent;}#mermaid-svg-fNitaMzgceGvcvLb .label text,#mermaid-svg-fNitaMzgceGvcvLb span{fill:#333;color:#333;}#mermaid-svg-fNitaMzgceGvcvLb .node rect,#mermaid-svg-fNitaMzgceGvcvLb .node circle,#mermaid-svg-fNitaMzgceGvcvLb .node ellipse,#mermaid-svg-fNitaMzgceGvcvLb .node polygon,#mermaid-svg-fNitaMzgceGvcvLb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fNitaMzgceGvcvLb .rough-node .label text,#mermaid-svg-fNitaMzgceGvcvLb .node .label text,#mermaid-svg-fNitaMzgceGvcvLb .image-shape .label,#mermaid-svg-fNitaMzgceGvcvLb .icon-shape .label{text-anchor:middle;}#mermaid-svg-fNitaMzgceGvcvLb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fNitaMzgceGvcvLb .rough-node .label,#mermaid-svg-fNitaMzgceGvcvLb .node .label,#mermaid-svg-fNitaMzgceGvcvLb .image-shape .label,#mermaid-svg-fNitaMzgceGvcvLb .icon-shape .label{text-align:center;}#mermaid-svg-fNitaMzgceGvcvLb .node.clickable{cursor:pointer;}#mermaid-svg-fNitaMzgceGvcvLb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fNitaMzgceGvcvLb .arrowheadPath{fill:#333333;}#mermaid-svg-fNitaMzgceGvcvLb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fNitaMzgceGvcvLb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fNitaMzgceGvcvLb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fNitaMzgceGvcvLb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fNitaMzgceGvcvLb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fNitaMzgceGvcvLb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fNitaMzgceGvcvLb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fNitaMzgceGvcvLb .cluster text{fill:#333;}#mermaid-svg-fNitaMzgceGvcvLb .cluster span{color:#333;}#mermaid-svg-fNitaMzgceGvcvLb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fNitaMzgceGvcvLb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fNitaMzgceGvcvLb rect.text{fill:none;stroke-width:0;}#mermaid-svg-fNitaMzgceGvcvLb .icon-shape,#mermaid-svg-fNitaMzgceGvcvLb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fNitaMzgceGvcvLb .icon-shape p,#mermaid-svg-fNitaMzgceGvcvLb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fNitaMzgceGvcvLb .icon-shape .label rect,#mermaid-svg-fNitaMzgceGvcvLb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fNitaMzgceGvcvLb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fNitaMzgceGvcvLb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fNitaMzgceGvcvLb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 工牌有效
工牌无效
普通员工
管理员
访客
用户进入公司
刷工牌
✅ 认证通过

确认是公司员工
❌ 认证失败

禁止入内
刷卡进入房间
办公区
办公区 + 机房
仅限大堂

1.3 在 Web 应用中

#mermaid-svg-OzjXzi8kMZmmu3IN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OzjXzi8kMZmmu3IN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OzjXzi8kMZmmu3IN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OzjXzi8kMZmmu3IN .error-icon{fill:#552222;}#mermaid-svg-OzjXzi8kMZmmu3IN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OzjXzi8kMZmmu3IN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OzjXzi8kMZmmu3IN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OzjXzi8kMZmmu3IN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OzjXzi8kMZmmu3IN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OzjXzi8kMZmmu3IN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OzjXzi8kMZmmu3IN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OzjXzi8kMZmmu3IN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OzjXzi8kMZmmu3IN .marker.cross{stroke:#333333;}#mermaid-svg-OzjXzi8kMZmmu3IN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OzjXzi8kMZmmu3IN p{margin:0;}#mermaid-svg-OzjXzi8kMZmmu3IN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OzjXzi8kMZmmu3IN .cluster-label text{fill:#333;}#mermaid-svg-OzjXzi8kMZmmu3IN .cluster-label span{color:#333;}#mermaid-svg-OzjXzi8kMZmmu3IN .cluster-label span p{background-color:transparent;}#mermaid-svg-OzjXzi8kMZmmu3IN .label text,#mermaid-svg-OzjXzi8kMZmmu3IN span{fill:#333;color:#333;}#mermaid-svg-OzjXzi8kMZmmu3IN .node rect,#mermaid-svg-OzjXzi8kMZmmu3IN .node circle,#mermaid-svg-OzjXzi8kMZmmu3IN .node ellipse,#mermaid-svg-OzjXzi8kMZmmu3IN .node polygon,#mermaid-svg-OzjXzi8kMZmmu3IN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OzjXzi8kMZmmu3IN .rough-node .label text,#mermaid-svg-OzjXzi8kMZmmu3IN .node .label text,#mermaid-svg-OzjXzi8kMZmmu3IN .image-shape .label,#mermaid-svg-OzjXzi8kMZmmu3IN .icon-shape .label{text-anchor:middle;}#mermaid-svg-OzjXzi8kMZmmu3IN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OzjXzi8kMZmmu3IN .rough-node .label,#mermaid-svg-OzjXzi8kMZmmu3IN .node .label,#mermaid-svg-OzjXzi8kMZmmu3IN .image-shape .label,#mermaid-svg-OzjXzi8kMZmmu3IN .icon-shape .label{text-align:center;}#mermaid-svg-OzjXzi8kMZmmu3IN .node.clickable{cursor:pointer;}#mermaid-svg-OzjXzi8kMZmmu3IN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OzjXzi8kMZmmu3IN .arrowheadPath{fill:#333333;}#mermaid-svg-OzjXzi8kMZmmu3IN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OzjXzi8kMZmmu3IN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OzjXzi8kMZmmu3IN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OzjXzi8kMZmmu3IN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OzjXzi8kMZmmu3IN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OzjXzi8kMZmmu3IN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OzjXzi8kMZmmu3IN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OzjXzi8kMZmmu3IN .cluster text{fill:#333;}#mermaid-svg-OzjXzi8kMZmmu3IN .cluster span{color:#333;}#mermaid-svg-OzjXzi8kMZmmu3IN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OzjXzi8kMZmmu3IN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OzjXzi8kMZmmu3IN rect.text{fill:none;stroke-width:0;}#mermaid-svg-OzjXzi8kMZmmu3IN .icon-shape,#mermaid-svg-OzjXzi8kMZmmu3IN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OzjXzi8kMZmmu3IN .icon-shape p,#mermaid-svg-OzjXzi8kMZmmu3IN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OzjXzi8kMZmmu3IN .icon-shape .label rect,#mermaid-svg-OzjXzi8kMZmmu3IN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OzjXzi8kMZmmu3IN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OzjXzi8kMZmmu3IN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OzjXzi8kMZmmu3IN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 授权(Authorization)
有管理员权限
无权限
GET /api/admin/users
解析 Token
检查权限
✅ 200 放行
❌ 403 拒绝
认证(Authentication)
POST /api/auth/login
验证用户名密码
返回 JWT Token


二、Spring Security 架构

2.1 FilterChain 过滤器链

Spring Security 的核心是一条过滤器链(FilterChain)。每个 HTTP 请求都要经过这些过滤器:
#mermaid-svg-JwvxfQX1OlFwgfLs{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JwvxfQX1OlFwgfLs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JwvxfQX1OlFwgfLs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JwvxfQX1OlFwgfLs .error-icon{fill:#552222;}#mermaid-svg-JwvxfQX1OlFwgfLs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JwvxfQX1OlFwgfLs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JwvxfQX1OlFwgfLs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JwvxfQX1OlFwgfLs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JwvxfQX1OlFwgfLs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JwvxfQX1OlFwgfLs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JwvxfQX1OlFwgfLs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JwvxfQX1OlFwgfLs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JwvxfQX1OlFwgfLs .marker.cross{stroke:#333333;}#mermaid-svg-JwvxfQX1OlFwgfLs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JwvxfQX1OlFwgfLs p{margin:0;}#mermaid-svg-JwvxfQX1OlFwgfLs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JwvxfQX1OlFwgfLs .cluster-label text{fill:#333;}#mermaid-svg-JwvxfQX1OlFwgfLs .cluster-label span{color:#333;}#mermaid-svg-JwvxfQX1OlFwgfLs .cluster-label span p{background-color:transparent;}#mermaid-svg-JwvxfQX1OlFwgfLs .label text,#mermaid-svg-JwvxfQX1OlFwgfLs span{fill:#333;color:#333;}#mermaid-svg-JwvxfQX1OlFwgfLs .node rect,#mermaid-svg-JwvxfQX1OlFwgfLs .node circle,#mermaid-svg-JwvxfQX1OlFwgfLs .node ellipse,#mermaid-svg-JwvxfQX1OlFwgfLs .node polygon,#mermaid-svg-JwvxfQX1OlFwgfLs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JwvxfQX1OlFwgfLs .rough-node .label text,#mermaid-svg-JwvxfQX1OlFwgfLs .node .label text,#mermaid-svg-JwvxfQX1OlFwgfLs .image-shape .label,#mermaid-svg-JwvxfQX1OlFwgfLs .icon-shape .label{text-anchor:middle;}#mermaid-svg-JwvxfQX1OlFwgfLs .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JwvxfQX1OlFwgfLs .rough-node .label,#mermaid-svg-JwvxfQX1OlFwgfLs .node .label,#mermaid-svg-JwvxfQX1OlFwgfLs .image-shape .label,#mermaid-svg-JwvxfQX1OlFwgfLs .icon-shape .label{text-align:center;}#mermaid-svg-JwvxfQX1OlFwgfLs .node.clickable{cursor:pointer;}#mermaid-svg-JwvxfQX1OlFwgfLs .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JwvxfQX1OlFwgfLs .arrowheadPath{fill:#333333;}#mermaid-svg-JwvxfQX1OlFwgfLs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JwvxfQX1OlFwgfLs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JwvxfQX1OlFwgfLs .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JwvxfQX1OlFwgfLs .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JwvxfQX1OlFwgfLs .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JwvxfQX1OlFwgfLs .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JwvxfQX1OlFwgfLs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JwvxfQX1OlFwgfLs .cluster text{fill:#333;}#mermaid-svg-JwvxfQX1OlFwgfLs .cluster span{color:#333;}#mermaid-svg-JwvxfQX1OlFwgfLs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JwvxfQX1OlFwgfLs .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JwvxfQX1OlFwgfLs rect.text{fill:none;stroke-width:0;}#mermaid-svg-JwvxfQX1OlFwgfLs .icon-shape,#mermaid-svg-JwvxfQX1OlFwgfLs .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JwvxfQX1OlFwgfLs .icon-shape p,#mermaid-svg-JwvxfQX1OlFwgfLs .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JwvxfQX1OlFwgfLs .icon-shape .label rect,#mermaid-svg-JwvxfQX1OlFwgfLs .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JwvxfQX1OlFwgfLs .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JwvxfQX1OlFwgfLs .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JwvxfQX1OlFwgfLs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTP 请求
SecurityFilterChain
SecurityContextPersistenceFilter

← 恢复安全上下文
UsernamePasswordAuthenticationFilter

← 处理表单登录
JwtAuthenticationFilter(自定义)

← 处理 JWT 认证
AuthorizationFilter

← 授权检查(最后一步)
DispatcherServlet → Controller

← 到达业务代码

2.2 核心对象

对象 作用
SecurityContext 安全上下文,持有当前用户信息
Authentication 认证信息(身份 + 凭证 + 权限)
UserDetails 用户详情接口(用户名、密码、权限)
UserDetailsService 加载用户信息的服务
GrantedAuthority 权限(如 ROLE_ADMIN
SecurityFilterChain 安全规则配置(哪些路径要保护)

2.3 认证流程

#mermaid-svg-D6XuWdMHjp4mbkHE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-D6XuWdMHjp4mbkHE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-D6XuWdMHjp4mbkHE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-D6XuWdMHjp4mbkHE .error-icon{fill:#552222;}#mermaid-svg-D6XuWdMHjp4mbkHE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-D6XuWdMHjp4mbkHE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-D6XuWdMHjp4mbkHE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-D6XuWdMHjp4mbkHE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-D6XuWdMHjp4mbkHE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-D6XuWdMHjp4mbkHE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-D6XuWdMHjp4mbkHE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-D6XuWdMHjp4mbkHE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-D6XuWdMHjp4mbkHE .marker.cross{stroke:#333333;}#mermaid-svg-D6XuWdMHjp4mbkHE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-D6XuWdMHjp4mbkHE p{margin:0;}#mermaid-svg-D6XuWdMHjp4mbkHE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-D6XuWdMHjp4mbkHE .cluster-label text{fill:#333;}#mermaid-svg-D6XuWdMHjp4mbkHE .cluster-label span{color:#333;}#mermaid-svg-D6XuWdMHjp4mbkHE .cluster-label span p{background-color:transparent;}#mermaid-svg-D6XuWdMHjp4mbkHE .label text,#mermaid-svg-D6XuWdMHjp4mbkHE span{fill:#333;color:#333;}#mermaid-svg-D6XuWdMHjp4mbkHE .node rect,#mermaid-svg-D6XuWdMHjp4mbkHE .node circle,#mermaid-svg-D6XuWdMHjp4mbkHE .node ellipse,#mermaid-svg-D6XuWdMHjp4mbkHE .node polygon,#mermaid-svg-D6XuWdMHjp4mbkHE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-D6XuWdMHjp4mbkHE .rough-node .label text,#mermaid-svg-D6XuWdMHjp4mbkHE .node .label text,#mermaid-svg-D6XuWdMHjp4mbkHE .image-shape .label,#mermaid-svg-D6XuWdMHjp4mbkHE .icon-shape .label{text-anchor:middle;}#mermaid-svg-D6XuWdMHjp4mbkHE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-D6XuWdMHjp4mbkHE .rough-node .label,#mermaid-svg-D6XuWdMHjp4mbkHE .node .label,#mermaid-svg-D6XuWdMHjp4mbkHE .image-shape .label,#mermaid-svg-D6XuWdMHjp4mbkHE .icon-shape .label{text-align:center;}#mermaid-svg-D6XuWdMHjp4mbkHE .node.clickable{cursor:pointer;}#mermaid-svg-D6XuWdMHjp4mbkHE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-D6XuWdMHjp4mbkHE .arrowheadPath{fill:#333333;}#mermaid-svg-D6XuWdMHjp4mbkHE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-D6XuWdMHjp4mbkHE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-D6XuWdMHjp4mbkHE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D6XuWdMHjp4mbkHE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-D6XuWdMHjp4mbkHE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D6XuWdMHjp4mbkHE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-D6XuWdMHjp4mbkHE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-D6XuWdMHjp4mbkHE .cluster text{fill:#333;}#mermaid-svg-D6XuWdMHjp4mbkHE .cluster span{color:#333;}#mermaid-svg-D6XuWdMHjp4mbkHE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-D6XuWdMHjp4mbkHE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-D6XuWdMHjp4mbkHE rect.text{fill:none;stroke-width:0;}#mermaid-svg-D6XuWdMHjp4mbkHE .icon-shape,#mermaid-svg-D6XuWdMHjp4mbkHE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-D6XuWdMHjp4mbkHE .icon-shape p,#mermaid-svg-D6XuWdMHjp4mbkHE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-D6XuWdMHjp4mbkHE .icon-shape .label rect,#mermaid-svg-D6XuWdMHjp4mbkHE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-D6XuWdMHjp4mbkHE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-D6XuWdMHjp4mbkHE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-D6XuWdMHjp4mbkHE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 匹配成功
匹配失败
用户提交用户名密码
UsernamePasswordAuthenticationFilter

拦截请求
AuthenticationManager

.authenticate()
UserDetailsService

.loadUserByUsername()
从数据库查到用户信息
PasswordEncoder.matches()

对比密码
✅ 创建 Authentication

存入 SecurityContext
❌ 抛出异常

认证失败


三、添加 Spring Security

3.1 添加依赖

kotlin 复制代码
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-security")
}

3.2 默认行为

添加依赖后,Spring Boot 自动启用安全配置:

  • 所有接口都需要认证------未登录的请求返回 401
  • 自动生成一个用户 :用户名 user,密码在控制台输出(每次启动随机生成)
  • 默认登录页面http://localhost:8080/login
  • 默认登出http://localhost:8080/logout

控制台输出:

复制代码
Using generated security password: 8a7b3c2d-1e2f-3a4b-5c6d-7e8f9a0b1c2d

这个默认行为只适合快速测试。实际开发中需要自定义配置。


四、SecurityFilterChain 配置

4.1 Lambda DSL 风格(Spring Security 6+)

kotlin 复制代码
package com.example.minishop.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            // 1. 禁用 CSRF(REST API 不需要)
            .csrf { it.disable() }

            // 2. CORS 配置(启用前面写的 CORS 配置)
            .cors { }

            // 3. 会话管理:无状态(用 JWT,不用 Session)
            .sessionManagement {
                it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            }

            // 4. 授权规则
            .authorizeHttpRequests { auth ->
                auth
                    // 公开接口
                    .requestMatchers("/api/auth/**").permitAll()
                    .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
                    .requestMatchers("/swagger-ui/**").permitAll()
                    .requestMatchers("/v3/api-docs/**").permitAll()

                    // 管理员接口
                    .requestMatchers("/api/admin/**").hasRole("ADMIN")

                    // 其他所有接口需要认证
                    .anyRequest().authenticated()
            }

            // 5. 表单登录(REST API 通常禁用)
            .formLogin { it.disable() }

            // 6. HTTP Basic 认证(REST API 通常禁用)
            .httpBasic { it.disable() }

        return http.build()
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return BCryptPasswordEncoder()
    }
}

4.2 授权规则详解

kotlin 复制代码
.authorizeHttpRequests { auth ->
    auth
        .requestMatchers("/api/auth/**").permitAll()              // 完全公开
        .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()  // GET 请求公开
        .requestMatchers("/api/admin/**").hasRole("ADMIN")        // 需要 ADMIN 角色
        .requestMatchers("/api/orders/**").hasAnyRole("ADMIN", "USER") // ADMIN 或 USER
        .requestMatchers("/api/products").hasAuthority("PRODUCT_WRITE") // 需要特定权限
        .anyRequest().authenticated()                              // 其他需要登录
}
方法 含义
permitAll() 所有人可访问(包括未登录)
authenticated() 需要登录
hasRole("ADMIN") 需要 ADMIN 角色(自动加 ROLE_ 前缀)
hasAnyRole("ADMIN", "USER") 需要其中任一角色
hasAuthority("PRODUCT_WRITE") 需要指定权限(不加前缀)
denyAll() 拒绝所有人(调试用)

授权决策流程:
#mermaid-svg-kCIzX5T15qgmfmto{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kCIzX5T15qgmfmto .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kCIzX5T15qgmfmto .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kCIzX5T15qgmfmto .error-icon{fill:#552222;}#mermaid-svg-kCIzX5T15qgmfmto .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kCIzX5T15qgmfmto .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kCIzX5T15qgmfmto .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kCIzX5T15qgmfmto .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kCIzX5T15qgmfmto .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kCIzX5T15qgmfmto .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kCIzX5T15qgmfmto .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kCIzX5T15qgmfmto .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kCIzX5T15qgmfmto .marker.cross{stroke:#333333;}#mermaid-svg-kCIzX5T15qgmfmto svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kCIzX5T15qgmfmto p{margin:0;}#mermaid-svg-kCIzX5T15qgmfmto .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kCIzX5T15qgmfmto .cluster-label text{fill:#333;}#mermaid-svg-kCIzX5T15qgmfmto .cluster-label span{color:#333;}#mermaid-svg-kCIzX5T15qgmfmto .cluster-label span p{background-color:transparent;}#mermaid-svg-kCIzX5T15qgmfmto .label text,#mermaid-svg-kCIzX5T15qgmfmto span{fill:#333;color:#333;}#mermaid-svg-kCIzX5T15qgmfmto .node rect,#mermaid-svg-kCIzX5T15qgmfmto .node circle,#mermaid-svg-kCIzX5T15qgmfmto .node ellipse,#mermaid-svg-kCIzX5T15qgmfmto .node polygon,#mermaid-svg-kCIzX5T15qgmfmto .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kCIzX5T15qgmfmto .rough-node .label text,#mermaid-svg-kCIzX5T15qgmfmto .node .label text,#mermaid-svg-kCIzX5T15qgmfmto .image-shape .label,#mermaid-svg-kCIzX5T15qgmfmto .icon-shape .label{text-anchor:middle;}#mermaid-svg-kCIzX5T15qgmfmto .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kCIzX5T15qgmfmto .rough-node .label,#mermaid-svg-kCIzX5T15qgmfmto .node .label,#mermaid-svg-kCIzX5T15qgmfmto .image-shape .label,#mermaid-svg-kCIzX5T15qgmfmto .icon-shape .label{text-align:center;}#mermaid-svg-kCIzX5T15qgmfmto .node.clickable{cursor:pointer;}#mermaid-svg-kCIzX5T15qgmfmto .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kCIzX5T15qgmfmto .arrowheadPath{fill:#333333;}#mermaid-svg-kCIzX5T15qgmfmto .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kCIzX5T15qgmfmto .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kCIzX5T15qgmfmto .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kCIzX5T15qgmfmto .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kCIzX5T15qgmfmto .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kCIzX5T15qgmfmto .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kCIzX5T15qgmfmto .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kCIzX5T15qgmfmto .cluster text{fill:#333;}#mermaid-svg-kCIzX5T15qgmfmto .cluster span{color:#333;}#mermaid-svg-kCIzX5T15qgmfmto div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kCIzX5T15qgmfmto .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kCIzX5T15qgmfmto rect.text{fill:none;stroke-width:0;}#mermaid-svg-kCIzX5T15qgmfmto .icon-shape,#mermaid-svg-kCIzX5T15qgmfmto .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kCIzX5T15qgmfmto .icon-shape p,#mermaid-svg-kCIzX5T15qgmfmto .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kCIzX5T15qgmfmto .icon-shape .label rect,#mermaid-svg-kCIzX5T15qgmfmto .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kCIzX5T15qgmfmto .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kCIzX5T15qgmfmto .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kCIzX5T15qgmfmto :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} /api/auth/**
/api/admin/**


其他路径
已登录
未登录
HTTP 请求到达
请求路径匹配?
permitAll()

✅ 公开访问
hasRole('ADMIN')?
✅ 放行
❌ 403 Forbidden
authenticated()?
✅ 放行
❌ 401 Unauthorized

4.3 hasRole vs hasAuthority

kotlin 复制代码
// hasRole:自动加 ROLE_ 前缀
// 数据库中存的是 "ROLE_ADMIN"
.hasRole("ADMIN")         // 匹配 "ROLE_ADMIN"

// hasAuthority:精确匹配,不加前缀
// 数据库中存的是 "PRODUCT_WRITE"
.hasAuthority("PRODUCT_WRITE")   // 匹配 "PRODUCT_WRITE"

规范 :角色用 hasRole(加 ROLE_ 前缀),细粒度权限用 hasAuthority(不加前缀)。


五、UserDetailsService

5.1 自定义用户加载

Spring Security 需要知道你的用户数据怎么存储。通过实现 UserDetailsService

kotlin 复制代码
package com.example.minishop.security

import com.example.minishop.repository.UserRepository
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.User
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

@Service
class CustomUserDetailsService(
    private val userRepository: UserRepository
) : UserDetailsService {

    override fun loadUserByUsername(username: String): UserDetails {
        val user = userRepository.findByUsername(username)
            ?: throw UsernameNotFoundException("用户不存在:$username")

        return User.builder()
            .username(user.username)
            .password(user.password)           // 已加密的密码
            .roles("USER")                     // 简化:先固定角色
            .build()
    }
}

5.2 UserDetails 接口

UserDetails 是 Spring Security 理解「用户」的标准接口。上面的 User.builder() 构建的就是它的默认实现。核心方法:

方法 含义
getUsername() 用户名
getPassword() 密码(已加密)
getAuthorities() 权限列表
isEnabled() 账号是否启用
isAccountNonExpired() 账号是否未过期
isAccountNonLocked() 账号是否未锁定
isCredentialsNonExpired() 凭证是否未过期

六、内存用户演示

在不接入数据库之前,先用内存用户演示 Spring Security 的工作流程:

kotlin 复制代码
@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .csrf { it.disable() }
            .authorizeHttpRequests { auth ->
                auth
                    .requestMatchers("/api/public/**").permitAll()
                    .requestMatchers("/api/admin/**").hasRole("ADMIN")
                    .requestMatchers("/api/user/**").hasRole("USER")
                    .anyRequest().authenticated()
            }
            // 使用 HTTP Basic 认证(演示用,后面换 JWT)
            .httpBasic { }

        return http.build()
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return BCryptPasswordEncoder()
    }

    // 内存用户(演示用,生产环境从数据库加载)
    @Bean
    fun userDetailsService(passwordEncoder: PasswordEncoder): UserDetailsService {
        val admin = User.builder()
            .username("admin")
            .password(passwordEncoder.encode("admin123"))
            .roles("ADMIN")
            .build()

        val user = User.builder()
            .username("user")
            .password(passwordEncoder.encode("user123"))
            .roles("USER")
            .build()

        return InMemoryUserDetailsManager(admin, user)
    }
}

6.1 测试

bash 复制代码
# 公开接口(无需认证)
curl http://localhost:8080/api/public/hello
# ✅ 200

# 需要认证的接口(不带凭证)
curl http://localhost:8080/api/user/profile
# ❌ 401 Unauthorized

# 用 USER 凭证访问
curl -u user:user123 http://localhost:8080/api/user/profile
# ✅ 200

# 用 USER 凭证访问 ADMIN 接口
curl -u user:user123 http://localhost:8080/api/admin/dashboard
# ❌ 403 Forbidden

# 用 ADMIN 凭证访问
curl -u admin:admin123 http://localhost:8080/api/admin/dashboard
# ✅ 200
状态码 含义
401 Unauthorized 未认证(没登录)
403 Forbidden 已认证但无权限(登录了但角色不够)

七、获取当前登录用户

在任何地方获取当前认证用户:

kotlin 复制代码
import org.springframework.security.core.context.SecurityContextHolder

@RestController
@RequestMapping("/api/user")
class UserController {

    @GetMapping("/profile")
    fun getProfile(): Map<String, Any?> {
        val authentication = SecurityContextHolder.getContext().authentication
        val username = authentication.name
        val authorities = authentication.authorities.map { it.authority }

        return mapOf(
            "username" to username,
            "authorities" to authorities
        )
    }
}

Kotlin 更优雅的方式------用扩展函数:

kotlin 复制代码
// 扩展函数:从 SecurityContext 获取当前用户名
fun Authentication?.getUsername(): String? = this?.name

// 使用
val username = SecurityContextHolder.getContext().authentication.getUsername()

本篇小结

知识点 核心内容
认证 「你是谁?」------验证身份
授权 「你能干嘛?」------检查权限
FilterChain 所有请求经过安全过滤器链
SecurityFilterChain Lambda DSL 配置安全规则
permitAll() 公开访问
authenticated() 需要登录
hasRole("ADMIN") 角色检查(自动加 ROLE_ 前缀)
hasAuthority("X") 权限检查(精确匹配)
UserDetailsService 加载用户信息
PasswordEncoder 密码加密(BCrypt)
401 vs 403 未认证 vs 已认证但无权限

下篇预告

第 31 篇:密码加密与用户注册

密码绝对不能明文存储!下一篇学习 BCrypt / Argon2 加密算法,实现安全的用户注册功能。


如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。

相关推荐
明月_清风43 分钟前
🚀 AI Agent 完全入门指南:从 LLM 到生产落地,新手必懂的 34 个核心概念
前端·后端·ai编程
程序员阿明1 小时前
spring boot3访问resources下的文件,使得在浏览器url中可以直接访问
java·spring boot·后端
掘金者阿豪1 小时前
MongoDB迁移不想重写代码?一次国产数据库替换踩坑记录
后端
Vec‑Jie1 小时前
MerchantOps-KBQA 实践(十四):本地运行、性能限制与可验证的交付边界
人工智能·后端·python·flask
qo_tn1 小时前
微信机器人-webhook技术文档_02-部署前准备与环境规划
后端
雪隐1 小时前
WPF + MVVM 实战系列02-告别 INPC 手写时代,做个体面的现代 WPF 人
前端·后端·c#
霸道流氓气质1 小时前
SpringBoot中使用OAuth2 认证与 JWT Token — 概念、原理与实践
java·spring boot·后端
dogstarhuang1 小时前
Kimi K3 本地部署实战:从 1.56TB 权重到推理服务的完整成本分析
java·人工智能·后端·ai·开源·接口·程序员创富
星栈1 小时前
我以为 TS7.0 只是换个版本号,结果编译快了 9 倍,也踩了 5 个坑
后端·typescript·node.js