基 于 角 色 权 限 模 型 与 数 据 权 限 控 制
以 小 明 的 摄 影 作 品 管 理 系 统 为 例 , 从 零 基 础 搭 建 到 完 整 认 证 鉴 权 , 逐 步 演 示 spring security 的 接 入 流 程 。 涵 盖 基 础 接 入 、 角 色 权 限 扩 展 、 数 据 权 限 控 制 三 大 模 块 , 每 步 标 注 自 定 义 类 与 spring security 扩 展 点 , 配 代 码 、 注 释 和 逻 辑 说 明 。 使 用 mybatisplus 作 为 orm 框 架 。
** 第 一 部 分 : 基 础 接 入 流 程 — 前 后 端 分 离 认 证 鉴 权 **
** 第 一 步 : 环 境 准 备 与 依 赖 引 入 **
** 目 的 **
搭 建 spring boot 基 础 项 目 , 引 入 spring security 及 前 后 端 分 离 必 需 组 件 , 使 用 mybatisplus 替 代 jpa 。
** 操 作 **
- ** 创 建 spring boot 项 目 **
- 通 过 start.spring.io 创 建 , 选 择 maven 、 java 17+ , 添 加 依 赖 spring web 、 spring security 、 mybatis plus 、 mysql driver 、 lombok 。
- ** pom.xml 核 心 依 赖 **
<dependencies>
<!-- spring web -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>
<!-- spring security -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-security</artifactid>
</dependency>
<!-- mybatis plus -->
<dependency>
<groupid>com.baomidou</groupid>
<artifactid>mybatis-plus-boot-starter</artifactid>
<version>3.5.3.1</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupid>com.mysql</groupid>
<artifactid>mysql-connector-j</artifactid>
<scope>runtime</scope>
</dependency>
<!-- jwt支持 -->
<dependency>
<groupid>io.jsonwebtoken</groupid>
<artifactid>jjwt-api</artifactid>
<version>0.11.5</version>
</dependency>
<dependency>
<groupid>io.jsonwebtoken</groupid>
<artifactid>jjwt-impl</artifactid>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupid>io.jsonwebtoken</groupid>
<artifactid>jjwt-jackson</artifactid>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- lombok -->
<dependency>
<groupid>org.projectlombok</groupid>
<artifactid>lombok</artifactid>
<optional>true</optional>
</dependency>
</dependencies> ** 第 二 步 : 数 据 库 设 计 与 实 体 类 **
** 目 的 **
设 计 用 户 、 角 色 表 , 并 让 用 户 po 类 实 现 spring security 的 userdetails 接 口 。
** 操 作 **
- ** 数 据 库 表 设 计 **
create table sys_user (
id bigint auto_increment primary key,
username varchar(50) not null unique comment '用户名',
password varchar(100) not null comment 'bcrypt加密密码',
email varchar(100) comment '邮箱',
enabled boolean default true comment '账户是否启用'
);
create table sys_role (
id bigint auto_increment primary key,
role_name varchar(50) not null unique comment '角色名称(无role_前缀)'
);
create table sys_user_role (
user_id bigint,
role_id bigint,
primary key (user_id, role_id),
foreign key (user_id) references sys_user(id),
foreign key (role_id) references sys_role(id)
); - ** 实 体 类 userpo 实 现 userdetails 接 口 **
import com.baomidou.mybatisplus.annotation.tablename;
import lombok.data;
import org.springframework.security.core.grantedauthority;
import org.springframework.security.core.authority.simplegrantedauthority;
import org.springframework.security.core.userdetails.userdetails;
import java.util.collection;
import java.util.set;
import java.util.stream.collectors;
@data
@tablename("sys_user")
public class userpo implements userdetails {
private long id;
private string username;
private string password;
private string email;
private boolean enabled = true;
private set<rolepo> roles;
@override
public collection<? extends grantedauthority> getauthorities() {
return roles.stream()
.map(role -> new simplegrantedauthority("role_" + role.getrolename()))
.collect(collectors.tolist());
}
@override
public boolean isaccountnonexpired() { return true; }
@override
public boolean isaccountnonlocked() { return true; }
@override
public boolean iscredentialsnonexpired() { return true; }
@override
public boolean isenabled() { return enabled; }
} - ** 角 色 实 体 类 rolepo **
import com.baomidou.mybatisplus.annotation.tablename;
import lombok.data;
@data
@tablename("sys_role")
public class rolepo {
private long id;
private string rolename;
} ** 第 三 步 : mybatisplus mapper 层 **
** 目 的 **
使 用 mybatisplus mapper 操 作 数 据 库 。
** 操 作 **
- ** usermapper 接 口 **
import com.baomidou.mybatisplus.core.mapper.basemapper;
import org.apache.ibatis.annotations.param;
import java.util.optional;
public interface usermapper extends basemapper<userpo> {
optional<userpo> findbyusername(@param("username") string username);
} - ** rolemapper 接 口 **
import com.baomidou.mybatisplus.core.mapper.basemapper;
import org.apache.ibatis.annotations.param;
public interface rolemapper extends basemapper<rolepo> {
optional<rolepo> findbyrolename(@param("rolename") string rolename);
} ** 第 四 步 : service 层 — 实 现 userdetailsservice **
** 目 的 **
实 现 spring security 的 userdetailsservice 接 口 , 配 置 密 码 编 码 器 。
** 操 作 **
- ** 自 定 义 userdetailsservice 实 现 类 **
import lombok.requiredargsconstructor;
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
@requiredargsconstructor
public class userdetailsserviceimpl implements userdetailsservice {
private final usermapper usermapper;
@override
public userdetails loaduserbyusername(string username) throws usernamenotfoundexception {
userpo user = usermapper.findbyusername(username)
.orelsethrow(() -> new usernamenotfoundexception("用户不存在:" + username));
return user;
}
} - ** 配 置 密 码 编 码 器 **
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.security.crypto.bcrypt.bcryptpasswordencoder;
import org.springframework.security.crypto.password.passwordencoder;
@configuration
public class passwordencoderconfig {
@bean
public passwordencoder passwordencoder() {
return new bcryptpasswordencoder();
}
} ** 第 五 步 : spring security 配 置 类 **
** 目 的 **
配 置 认 证 规 则 、 授 权 规 则 、 jwt 过 滤 器 。
** 操 作 **
- ** 自 定 义 security 配 置 类 **
import lombok.requiredargsconstructor;
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.security.authentication.authenticationmanager;
import org.springframework.security.config.annotation.authentication.configuration.authenticationconfiguration;
import org.springframework.security.config.annotation.method.configuration.enableglobalmethodsecurity;
import org.springframework.security.config.annotation.web.builders.httpsecurity;
import org.springframework.security.config.annotation.web.configuration.enablewebsecurity;
import org.springframework.security.config.http.sessioncreationpolicy;
import org.springframework.security.crypto.password.passwordencoder;
import org.springframework.security.web.securityfilterchain;
import org.springframework.security.web.authentication.usernamepasswordauthenticationfilter;
@configuration
@enablewebsecurity
@enableglobalmethodsecurity(prepostenabled = true)
@requiredargsconstructor
public class securityconfig {
private final userdetailsserviceimpl userdetailsservice;
private final jwtauthfilter jwtauthfilter;
private final passwordencoder passwordencoder;
@bean
public securityfilterchain securityfilterchain(httpsecurity http) throws exception {
http.csrf(csrf -> csrf.disable())
.sessionmanagement(session -> session.sessioncreationpolicy(sessioncreationpolicy.stateless))
.authorizehttprequests(auth -> auth
.requestmatchers("/api/auth/login", "/api/auth/register").permitall()
.requestmatchers("/api/admin/**").hasrole("admin")
.requestmatchers("/api/user/**").hasanyrole("admin", "user")
.anyrequest().authenticated()
)
.addfilterbefore(jwtauthfilter, usernamepasswordauthenticationfilter.class)
.userdetailsservice(userdetailsservice)
.passwordencoder(passwordencoder);
return http.build();
}
@bean
public authenticationmanager authenticationmanager(authenticationconfiguration config) throws exception {
return config.getauthenticationmanager();
}
} ** 第 六 步 : jwt 工 具 类 与 过 滤 器 **
** 目 的 **
实 现 jwt 生 成 、 验 证 、 提 取 用 户 名 , 并 通 过 过 滤 器 转 换 为 认 证 信 息 。
** 操 作 **
- ** jwt 工 具 类 jwtutils **
import io.jsonwebtoken.claims;
import io.jsonwebtoken.jwts;
import io.jsonwebtoken.signaturealgorithm;
import org.springframework.beans.factory.annotation.value;
import org.springframework.security.core.userdetails.userdetails;
import org.springframework.stereotype.component;
import java.util.date;
import java.util.function.function;
@component
public class jwtutils {
@value("${app.jwt.secret}")
private string secret;
@value("${app.jwt.expiration}")
private long expiration;
public string generatetoken(userdetails userdetails) {
return jwts.builder()
.setsubject(userdetails.getusername())
.setissuedat(new date())
.setexpiration(new date(system.currenttimemillis() + expiration))
.signwith(signaturealgorithm.hs256, secret)
.compact();
}
public string extractusername(string token) {
return extractclaim(token, claims::getsubject);
}
public boolean validatetoken(string token, userdetails userdetails) {
final string username = extractusername(token);
return (username.equals(userdetails.getusername())) && !istokenexpired(token);
}
private <t> t extractclaim(string token, function<claims, t> claimsresolver) {
final claims claims = jwts.parser().setsigningkey(secret).parseclaimsjws(token).getbody();
return claimsresolver.apply(claims);
}
private boolean istokenexpired(string token) {
return extractclaim(token, claims::getexpiration).before(new date());
}
} - ** jwt 认 证 过 滤 器 jwtauthfilter **
import lombok.requiredargsconstructor;
import org.springframework.security.authentication.usernamepasswordauthenticationtoken;
import org.springframework.security.core.context.securitycontextholder;
import org.springframework.security.core.userdetails.userdetails;
import org.springframework.security.web.authentication.webauthenticationdetailssource;
import org.springframework.stereotype.component;
import org.springframework.web.filter.onceperrequestfilter;
import javax.servlet.filterchain;
import javax.servlet.servletexception;
import javax.servlet.http.httpservletrequest;
import javax.servlet.http.httpservletresponse;
import java.io.ioexception;
@component
@requiredargsconstructor
public class jwtauthfilter extends onceperrequestfilter {
private final jwtutils jwtutils;
private final userdetailsserviceimpl userdetailsservice;
@override
protected void dofilterinternal(httpservletrequest request, httpservletresponse response, filterchain filterchain)
throws servletexception, ioexception {
try {
string jwt = parsejwt(request);
if (jwt != null && jwtutils.validatetoken(jwt)) {
string username = jwtutils.extractusername(jwt);
userdetails userdetails = userdetailsservice.loaduserbyusername(username);
usernamepasswordauthenticationtoken authentication =
new usernamepasswordauthenticationtoken(userdetails, null, userdetails.getauthorities());
authentication.setdetails(new webauthenticationdetailssource().builddetails(request));
securitycontextholder.getcontext().setauthentication(authentication);
}
} catch (exception e) {
logger.error("无法设置用户认证: {}", e);
}
filterchain.dofilter(request, response);
}
private string parsejwt(httpservletrequest request) {
string headerauth = request.getheader("authorization");
if (headerauth != null && headerauth.startswith("bearer ")) {
return headerauth.substring(7);
}
return null;
}
} ** 第 七 步 : 登 录 接 口 与 资 源 接 口 **
** 目 的 **
编 写 登 录 接 口 认 证 并 生 成 token , 编 写 受 保 护 资 源 接 口 。
** 操 作 **
- ** 登 录 控 制 器 authcontroller **
import lombok.requiredargsconstructor;
import org.springframework.http.responseentity;
import org.springframework.security.authentication.authenticationmanager;
import org.springframework.security.authentication.usernamepasswordauthenticationtoken;
import org.springframework.security.core.authentication;
import org.springframework.security.core.userdetails.userdetails;
import org.springframework.web.bind.annotation.*;
@restcontroller
@requestmapping("/api/auth")
@requiredargsconstructor
public class authcontroller {
private final authenticationmanager authenticationmanager;
private final jwtutils jwtutils;
@postmapping("/login")
public responseentity<jwtresponse> login(@requestbody loginrequest loginrequest) {
authentication authentication = authenticationmanager.authenticate(
new usernamepasswordauthenticationtoken(
loginrequest.getusername(),
loginrequest.getpassword()
)
);
securitycontextholder.getcontext().setauthentication(authentication);
userdetails userdetails = (userdetails) authentication.getprincipal();
string jwt = jwtutils.generatetoken(userdetails);
return responseentity.ok(new jwtresponse(jwt, userdetails.getusername()));
}
} ** 第 二 部 分 : 权 限 表 扩 展 指 南 — 基 于 角 色 权 限 模 型 **
** 一 、 数 据 库 扩 展 **
** 目 的 **
新 增 权 限 表 实 现 细 粒 度 权 限 控 制 。
** 操 作 **
- ** 新 增 权 限 表 结 构 **
create table sys_permission (
id bigint auto_increment primary key,
permission_symbol varchar(100) not null unique comment '权限符号(如 user:add)',
permission_name varchar(100) not null comment '权限名称'
);
create table sys_role_permission (
role_id bigint,
permission_id bigint,
primary key (role_id, permission_id),
foreign key (role_id) references sys_role(id),
foreign key (permission_id) references sys_permission(id)
); ** 二 、 实 体 类 扩 展 **
** 目 的 **
修 改 角 色 与 用 户 实 体 类 关 联 权 限 。
** 操 作 **
- ** 新 增 权 限 实 体 类 permissionpo **
import com.baomidou.mybatisplus.annotation.tablename;
import lombok.data;
import java.util.set;
@data
@tablename("sys_permission")
public class permissionpo {
private long id;
private string permissionsymbol;
private string permissionname;
private set<rolepo> roles;
} - ** 修 改 角 色 实 体 类 rolepo **
import com.baomidou.mybatisplus.annotation.tablename;
import lombok.data;
import java.util.set;
@data
@tablename("sys_role")
public class rolepo {
private long id;
private string rolename;
private set<permissionpo> permissions;
private set<userpo> users;
} - ** 修 改 用 户 实 体 类 userpo 权 限 加 载 **
@override
public collection<? extends grantedauthority> getauthorities() {
set<grantedauthority> authorities = new hashset<>();
for (rolepo role : roles) {
authorities.add(new simplegrantedauthority("role_" + role.getrolename()));
for (permissionpo permission : role.getpermissions()) {
authorities.add(new simplegrantedauthority(permission.getpermissionsymbol()));
}
}
return authorities;
} ** 三 、 mybatisplus mapper 扩 展 **
** 目 的 **
添 加 权 限 相 关 mapper 接 口 。
** 操 作 **
- ** permissionmapper **
import com.baomidou.mybatisplus.core.mapper.basemapper;
import org.apache.ibatis.annotations.param;
import java.util.optional;
public interface permissionmapper extends basemapper<permissionpo> {
optional<permissionpo> findbypermissionsymbol(@param("symbol") string symbol);
} ** 四 、 service 层 扩 展 **
** 目 的 **
实 现 权 限 管 理 服 务 。
** 操 作 **
- ** 权 限 管 理 服 务 permissionservice **
import lombok.requiredargsconstructor;
import org.springframework.stereotype.service;
import org.springframework.transaction.annotation.transactional;
import java.util.arrays;
import java.util.stream.collectors;
@service
@requiredargsconstructor
public class permissionservice {
private final permissionmapper permissionmapper;
private final rolemapper rolemapper;
@transactional
public void assignpermissiontorole(long roleid, string permissionsymbol) {
rolepo role = rolemapper.selectbyid(roleid);
permissionpo permission = permissionmapper.findbypermissionsymbol(permissionsymbol)
.orelseget(() -> {
permissionpo p = new permissionpo();
p.setpermissionsymbol(permissionsymbol);
p.setpermissionname(parsepermissionname(permissionsymbol));
permissionmapper.insert(p);
return p;
});
role.getpermissions().add(permission);
rolemapper.updatebyid(role);
}
private string parsepermissionname(string symbol) {
return arrays.stream(symbol.split(":"))
.map(word -> word.substring(0, 1).touppercase() + word.substring(1))
.collect(collectors.joining(" "));
}
} ** 五 、 控 制 器 层 扩 展 — 使 用 权 限 注 解 **
** 目 的 **
通 过 @preauthorize 注 解 控 制 方 法 权 限 。
** 操 作 **
- ** 用 户 控 制 器 使 用 权 限 注 解 **
import org.springframework.security.access.prepost.preauthorize;
import org.springframework.web.bind.annotation.*;
@restcontroller
@requestmapping("/api/users")
public class usercontroller {
@postmapping
@preauthorize("hasauthority('user:add')")
public responseentity<?> adduser() {
return responseentity.ok("添加用户");
}
@deletemapping("/{id}")
@preauthorize("hasauthority('user:delete')")
public responseentity<?> deleteuser(@pathvariable long id) {
return responseentity.ok("删除用户");
}
} ** 第 三 部 分 : 数 据 权 限 控 制 — 订 单 查 询 只 查 看 自 己 创 建 的 订 单 **
** 一 、 数 据 库 扩 展 — 订 单 表 **
** 目 的 **
设 计 订 单 表 关 联 创 建 人 。
** 操 作 **
- ** 订 单 表 结 构 **
create table sys_order (
id bigint auto_increment primary key,
order_no varchar(50) not null comment '订单编号',
amount decimal(10,2) not null comment '订单金额',
creator_id bigint not null comment '创建人id',
created_at timestamp default current_timestamp,
foreign key (creator_id) references sys_user(id)
); - ** 订 单 实 体 类 orderpo **
import com.baomidou.mybatisplus.annotation.tablename;
import lombok.data;
import java.math.bigdecimal;
import java.time.localdatetime;
@data
@tablename("sys_order")
public class orderpo {
private long id;
private string orderno;
private bigdecimal amount;
private long creatorid;
private localdatetime createdat;
} ** 二 、 mybatisplus mapper 与 service **
** 目 的 **
实 现 订 单 数 据 访 问 与 权 限 过 滤 。
** 操 作 **
- ** ordermapper **
import com.baomidou.mybatisplus.core.mapper.basemapper;
import org.apache.ibatis.annotations.param;
import java.util.list;
public interface ordermapper extends basemapper<orderpo> {
list<orderpo> findbycreatorid(@param("creatorid") long creatorid);
} - ** 订 单 服 务 实 现 **
import lombok.requiredargsconstructor;
import org.springframework.security.core.context.securitycontextholder;
import org.springframework.stereotype.service;
import java.util.list;
@service
@requiredargsconstructor
public class orderserviceimpl implements orderservice {
private final ordermapper ordermapper;
private final usermapper usermapper;
@override
public list<orderpo> findmyorders() {
userpo currentuser = getcurrentuser();
return ordermapper.findbycreatorid(currentuser.getid());
}
@override
public orderpo findorderbyid(long id) {
orderpo order = ordermapper.selectbyid(id);
userpo currentuser = getcurrentuser();
if (!order.getcreatorid().equals(currentuser.getid())) {
throw new accessdeniedexception("无权访问此订单");
}
return order;
}
private userpo getcurrentuser() {
string username = securitycontextholder.getcontext().getauthentication().getname();
return usermapper.findbyusername(username).orelsethrow();
}
} ** 三 、 控 制 器 层 实 现 **
** 目 的 **
编 写 订 单 相 关 接 口 , 集 成 数 据 权 限 控 制 。
** 操 作 **
- ** 订 单 控 制 器 **
import org.springframework.security.access.prepost.preauthorize;
import org.springframework.web.bind.annotation.*;
@restcontroller
@requestmapping("/api/orders")
@requiredargsconstructor
public class ordercontroller {
private final orderservice orderservice;
@getmapping
@preauthorize("hasauthority('order:view')")
public responseentity<list<orderpo>> getmyorders() {
list<orderpo> orders = orderservice.findmyorders();
return responseentity.ok(orders);
}
@getmapping("/{id}")
@preauthorize("hasauthority('order:view')")
public responseentity<orderpo> getorderbyid(@pathvariable long id) {
orderpo order = orderservice.findorderbyid(id);
return responseentity.ok(order);
}
} ** 总 结 **
通 过 以 上 三 部 分 整 合 , 我 们 实 现 了 spring security 前 后 端 分 离 接 入 、 角 色 权 限 扩 展 、 数 据 权 限 控 制 的 完 整 流 程 。 核 心 要 点 如 下 :
- ** 基 础 接 入 ** : 实 现 用 户 认 证 与 jwt 授 权 。
- ** 权 限 扩 展 ** : 通 过 权 限 表 与 角 色 关 联 , 实 现 细 粒 度 权 限 控 制 。
- ** 数 据 权 限 ** : 在 service 层 手 动 过 滤 数 据 , 确 保 用 户 只 能 查 看 自 己 创 建 的 订 单 。
整 个 系 统 兼 顾 了 安 全 性 与 灵 活 性 , 适 用 于 中 小 型 项 目 的 权 限 管 理 需 求 。
到此这篇关于spring security前后端分离接入流程保姆级教程的文章就介绍到这了,更多相关spring security前后端分离接入内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论