【Kotlin + Spring Boot 4 从零到架构师】第 30 篇:Spring Security 7 核心概念
本系列定位:零基础入门,从 Kotlin 语法一路到 Spring Boot 4 高级架构(DDD + Modulith),适合 Java 开发者转型,也适合纯新手系统学习。
本篇你将学到
- 认证(Authentication)与授权(Authorization)的区别
- Spring Security 的 FilterChain 架构
SecurityFilterChain配置(Lambda DSL)UserDetailsService与UserDetails- 权限表达式:
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 已认证但无权限 |
下篇预告
密码绝对不能明文存储!下一篇学习 BCrypt / Argon2 加密算法,实现安全的用户注册功能。
如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。