# liuwei-SpringSecurity
**Repository Path**: su27sk/liuwei-SpringSecurity
## Basic Information
- **Project Name**: liuwei-SpringSecurity
- **Description**: SpringSecurity
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-09-21
- **Last Updated**: 2026-10-13
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# SpringSecurity初体验
## 如何配置自定义用户名密码
```yaml
spring:
application:
name: security-01-base
# 自定义设置用户名密码
security:
user:
name: liuwei
password: aaa111
server:
port: 8081
```
## SpringSecurity查数据库登录
```java
@Service
public class UserServiceImpl implements UserService {
@Autowired
private TUserMapper tUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据usename去查询数据库
TUser tUser = tUserMapper.selectByLoginAct(username);
if (tUser == null) {
throw new UsernameNotFoundException("登录账号不存在");
}
UserDetails userDetail = User.builder()
.username(tUser.getLoginAct())
.password(tUser.getLoginPwd())
.authorities(AuthorityUtils.NO_AUTHORITIES) // 权限是空的
.build();
return userDetail;
}
}
```
## 自定义页面配置
配置类
```java
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.formLogin((formLogin) -> {
formLogin.loginProcessingUrl("/user/login") // 登录的账号密码往那个地址提交
.loginPage("/toLogin"); // 自定义登录页
})
// 把所有接口都会进行的登录状态检查的默认行为都加回来
.authorizeHttpRequests((authorizeHttpRequests) -> {
authorizeHttpRequests
.requestMatchers("/toLogin").permitAll() // 特殊接口:可以不登陆就访问
.anyRequest().authenticated(); // 任何后端接口的请求都需要认证
})
.build();
}
}
```
对应的自定义登录页面
```html
登录
```
## 验证码功能
引入hutool工具包的验证码依赖
```yaml
cn.hutool
hutool-captcha
5.8.32
```
自定义验证码字符串生成工具类
```java
/*
自定义验证码字符串生成
*/
public class MyCodeGenerator implements CodeGenerator {
@Override
public String generate() {
int code = 1000 + new Random().nextInt(9000);
return String.valueOf(code);
}
@Override
public boolean verify(String code, String userInputCode) {
return false;
}
}
```
请求拿到验证码的接口
```java
@Controller
public class CaptchaController {
@RequestMapping(value = "/common/captcha")
public void generateCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 设置验证码返回类型
response.setContentType("image/jpeg");
// 生成验证码图片
// ICaptcha captcha = CaptchaUtil.createCircleCaptcha(90, 30, 4, 10, 1);
ICaptcha captcha = CaptchaUtil.createCircleCaptcha(90, 30,new MyCodeGenerator(), 15);
// 将验证码的code保存起来
request.getSession().setAttribute("captcha", captcha.getCode());
// 返回给前端验证码图片
captcha.write(response.getOutputStream());
}
}
```
CaptchaFilter完成
```java
@Component
public class CaptchaFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String code = request.getParameter("captcha");
String sessionCode = (String) request.getSession().getAttribute("captcha");
String requestURI = request.getRequestURI();
// 判断是否是登录请求
if(requestURI.equals("/user/login")) {
if (!StringUtils.hasText(code)) {
// 验证没通过,重定向到登录页
response.sendRedirect("/");
} else if (!code.equalsIgnoreCase(sessionCode)) { // 忽略大小写比较两个字符串内容是否相等
response.sendRedirect("/");
} else {
// 验证通过
filterChain.doFilter(request, response);
}
} else {
// 不是登录请求直接放行
filterChain.doFilter(request, response);
}
}
}
```
```java
@Configuration
public class SecurityConfig {
@Resource
private CaptchaFilter captchaFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.formLogin((formLogin) -> {
formLogin.loginProcessingUrl("/user/login") // 登录的账号密码往那个地址提交
.loginPage("/toLogin"); // 自定义登录页
})
// 把所有接口都会进行的登录状态检查的默认行为都加回来
.authorizeHttpRequests((authorizeHttpRequests) -> {
authorizeHttpRequests
.requestMatchers("/toLogin", "/common/captcha").permitAll() // 特殊接口:可以不登陆就访问
.anyRequest().authenticated(); // 任何后端接口的请求都需要认证
})
// 验证码filter在接收登录账号密码之前
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
```
## 最后完成页面

## 获取用户登录身份
首先do需要继承UserDetails,然后实现他的方法
```java
@Data
public class TUser implements UserDetails,Serializable {
/**
* 主键,自动增长,用户ID
*/
private Integer id;
/**
* 登录账号
*/
private String loginAct;
/**
* 登录密码
*/
@JsonIgnore // 排除不想返回的字段
private String loginPwd;
/**
* 用户姓名
*/
private String name;
/**
* 用户手机
*/
private String phone;
/**
* 用户邮箱
*/
private String email;
/**
* 账户是否没有过期,0已过期 1正常
*/
private Integer accountNoExpired;
/**
* 密码是否没有过期,0已过期 1正常
*/
private Integer credentialsNoExpired;
/**
* 账号是否没有锁定,0已锁定 1正常
*/
private Integer accountNoLocked;
/**
* 账号是否启用,0禁用 1启用
*/
private Integer accountEnabled;
/**
* 创建时间
*/
// @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
/**
* 创建人
*/
private Integer createBy;
/**
* 编辑时间
*/
// @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date editTime;
/**
* 编辑人
*/
private Integer editBy;
/**
* 最近登录时间
*/
// @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date lastLoginTime;
private static final long serialVersionUID = 1L;
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return List.of();
}
@JsonIgnore
@Override
public String getPassword() {
return this.loginPwd;
}
@Override
public String getUsername() {
return this.loginAct;
}
@Override
public boolean isAccountNonExpired() {
return this.accountNoExpired == 1;
}
@Override
public boolean isAccountNonLocked() {
return this.accountNoLocked == 1;
}
@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNoExpired == 1;
}
@Override
public boolean isEnabled() {
return this.accountEnabled == 1;
}
}
```
然后数据库查询后直接返回对象就行
```java
@Service
public class UserServiceImpl implements UserService {
@Autowired
private TUserMapper tUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据usename去查询数据库
TUser tUser = tUserMapper.selectByLoginAct(username);
if (tUser == null) {
throw new UsernameNotFoundException("登录账号不存在");
}
return tUser;
}
}
```
然后封装工具类实现
```java
public class LoginInfoUtil {
public static TUser getCurrentLoginUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
TUser tUser = (TUser) authentication.getPrincipal();
return tUser;
}
}
```
最后调用
```java
@RequestMapping(value = "/welcome5")
public @ResponseBody Object welcome5() {
return LoginInfoUtil.getCurrentLoginUser();
}
```
# 角色权限配置
## 根据角色
首先数据库里面配置了t_role(角色表)和t_user_role(用户角色关系表),首先通过查表找出当前用户的角色
```xml
```
然后改造TUser实体类
```java
package com.liuwei.entity;
/**
* 用户表
* t_user
*/
@Data
public class TUser implements UserDetails,Serializable {
...
// 用户的角色权限
@JsonIgnore
private List tRoleList;
...
@Override
public Collection extends GrantedAuthority> getAuthorities() {
Collection authorities = new ArrayList<>();
for (TRole tRole : this.tRoleList) {
// 放入角色
authorities.add(new SimpleGrantedAuthority("ROLE_" + tRole.getRole()));
}
return authorities; // 返回用户的权限
}
}
```
给loadUserByUsername设置用户对应角色
```java
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据usename去查询数据库
TUser tUser = tUserMapper.selectByLoginAct(username);
if (tUser == null) {
throw new UsernameNotFoundException("登录账号不存在");
}
// 查询该用户的角色列表,一个用户可能有多个角色
List tRoleList = tRoleMapper.selectByUserId(tUser.getId());
tUser.setTRoleList(tRoleList);
return tUser;
}
```
给相关的接口添加注释@PreAuthorize(value = "hasAuthority('saler')")说明访问的角色权限,这个例子就算只有saler角色权限才有访问权限,不然报错402
```java
@PreAuthorize(value = "hasRole('saler')")
@RequestMapping(value = "/api/clue/menu")
public String clueMenu() {
return "clueMenu";
}
```
配置@EnableMethodSecurity注解生效
```java
@EnableMethodSecurity
public class SecurityConfig {
}
```
## 根据权限
```xml
```
用户查询权限然后配置
```java
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据usename去查询数据库
TUser tUser = tUserMapper.selectByLoginAct(username);
if (tUser == null) {
throw new UsernameNotFoundException("登录账号不存在");
}
// 查询该用户的权限列表
List tPermissionList = tPermissionMapper.selectByUserId(tUser.getId());
tUser.setTPermissionList(tPermissionList);
return tUser;
}
```
```java
@Data
public class TUser implements UserDetails,Serializable {
// 用户的权限
@JsonIgnore
List tPermissionList;
@Override
public Collection extends GrantedAuthority> getAuthorities() {
Collection authorities = new ArrayList<>();
/*for (TRole tRole : this.tRoleList) {
// 放入角色
authorities.add(new SimpleGrantedAuthority("ROLE_" + tRole.getRole()));
}*/
for (TPermission tPermission : tPermissionList) {
// 放入权限
authorities.add(new SimpleGrantedAuthority(tPermission.getCode()));
}
return authorities; // 返回用户的权限
}
}
```
控制层分配权限
```java
@PreAuthorize(value = "hasAuthority('clue:del')")
@RequestMapping(value = "/api/clue/del")
public String clueDel() {
return "clueDel";
}
@PreAuthorize(value = "hasAnyAuthority('clue:export','clue:download')") // admin manager角色可以访问
@RequestMapping(value = "/api/clue/export")
public String clueExport() {
return "clueExport";
}
```
# 前后端分离架构
## 后端
后端SecurityConfig修改
```java
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.formLogin((formLogin) -> {
formLogin.loginProcessingUrl("/user/login"); // 登录的账号密码往那个地址提交
})
// 把所有接口都会进行的登录状态检查的默认行为都加回来
.authorizeHttpRequests((authorizeHttpRequests) -> {
authorizeHttpRequests
.anyRequest().authenticated(); // 任何后端接口的请求都需要认证
})
.csrf((csrf) -> {
csrf.disable();
})
.build();
}
}
```
## 前端
前后端分离前端使用axios发送接口请求
```html
登录
```
### 解决跨域问题
什么叫做跨域问题
```html
1、协议不同会跨域 [https://localhost:8080](https://localhost:8080) [http://localhost:8080](http://localhost:8080)
2、端口不同会跨域: [http://localhost:10492](http://localhost:10492) [http://localhost:8080](http://localhost:8080)
3、域名不同会跨域: [http://bjpowernode.com](http://bjpowernode.com) [http://baidu.com](http://baidu.com)
```
```java
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
// 跨域配置
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowedOrigins(Arrays.asList("*")); // 允许任何来源
corsConfiguration.setAllowedMethods(Arrays.asList("*")); // 允许任何请求方法
corsConfiguration.setAllowedHeaders(Arrays.asList("*")); // 允许任何请求头
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration); // 允许任何路径
return urlBasedCorsConfigurationSource;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, CorsConfigurationSource corsConfigurationSource) throws Exception {
return httpSecurity
.formLogin((formLogin) -> {
formLogin.loginProcessingUrl("/user/login") // 登录的账号密码往那个地址提交
.successHandler(myAuthenticationSuccessHandler); // 返回结果的handler
})
// 把所有接口都会进行的登录状态检查的默认行为都加回来
.authorizeHttpRequests((authorizeHttpRequests) -> {
authorizeHttpRequests
.anyRequest().authenticated(); // 任何后端接口的请求都需要认证
})
.csrf((csrf) -> {
csrf.disable();
})
.cors((cors) -> {
cors.configurationSource(corsConfigurationSource); // 允许前端跨域访问
})
.build();
}
```
### 登陆成功后跳转页面
```javascript
axios.post('http://localhost:8081/user/login', formData).then(function (response) {
console.log(response)
// 登录成功完成页面跳转
if (response.data.code === 200) {
window.location.href = "welcome.html"
} else {
alert("登录失败,原因:" + response.data.msg)
}
}).catch(function (error) {
console.log(error)
});
}
```
### 登录成功/失败Handler
成功
```java
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
R result = R.builder().code(200).msg("登录成功").info(authentication).build();
String json = JSONUtil.toJsonStr(result);
response.getWriter().write(json);
}
}
```
失败
```java
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
R result = R.builder().code(500).msg("登录失败:" + exception.getMessage()).build();
String json = JSONUtil.toJsonStr(result);
response.getWriter().write(json);
}
}
```
## JWT
完整结构:**Header.Payload.Signature**,用英文点 `.` 分割三段
>
> 注意:默认 JWS 只是**签名防篡改,不是加密**,payload 是 Base64URL 编码,任何人都能解码看到内容,只是不能随便改。
## 第一段:Header 头部
作用:**声明用什么签名算法**
json 示例:
```
{
"alg":"HS256",
"typ":"JWT"
}
```
- `alg`:算法,HS256=HMAC-SHA256(对称,你代码里用的);RS256=RSA 非对称
- `typ`:类型,固定 JWT
处理:把这个 JSON 做 **Base64URL 编码**,得到第一段字符串。
## 第二段:Payload 载荷(Claims)
存放业务数据,**不要放密码、银行卡这种敏感数据!明文可解码**
json 示例:
```
{
"sub":"10001",
"name":"刘威",
"exp":1790000000
}
```
分为两类声明:
1. 标准保留声明(推荐)
- `exp` Expiration Time:过期时间戳(`jwtVerifier.verify`自动校验)
- `iat` Issued At:签发时间
- `nbf` Not Before:生效时间,在此之前 token 无效
- `sub` subject:主题,一般放用户 id
- `iss` 签发者、`aud`受众
2. 自定义声明:自己加字段,比如`userId`、`role`
处理:JSON → Base64URL 编码,得到第二段字符串。
## 第三段:Signature 签名(核心,防篡改)
>
> 签名不是加密!是生成指纹。
> **生成签名的原料:【第一段编码串。第二段编码串】 + 密钥 + 算法**
以你的 HMAC256 举例:
1. 拼接字符串:`data = base64(header) + "." + base64(payload)`
2. 使用 secret 密钥,执行 HMAC-SHA256,算出哈希摘要
3. 摘要 Base64URL 编码 → 第三段 Signature
## ✅ 签发流程(后端生成 token)
1. 构造 header、payload 两个 json
2. 分别 base64url 编码,拼接 `s1.s2`
3. secret + HMAC256 对`s1.s2`计算签名 s3
4. 最终 token:`s1.s2.s3` 返回前端
## ✅ 验签流程(就是你代码 `jwtVerifier.verify(token)`)
1. 按`.`切割 token,拿到三段 s1,s2,s3
2. 拿`s1.s2`,**用同一个 secret,同样 HMAC256 重新算出新签名**
3. 对比新签名 和 token 里的 s3
- 相等:说明 header、payload 没有被篡改;
- 不等:直接抛异常,token 无效。
4. 签名校验通过后,**再校验 exp 过期时间、nbf、iss 等声明**,全部通过返回 DecodedJWT。
## 结合Redis
```java
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private RedisTemplate redisTemplate;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
TUser tUser = (TUser) authentication.getPrincipal();
String userJSON = JSONUtil.toJsonStr(tUser);
String token = JWTUtil.createToken(Map.of("user", userJSON), Constant.SECRET.getBytes());
// 写入redis
redisTemplate.opsForHash().put(Constant.REDIS_TOKEN_KEY, String.valueOf(tUser.getId()), token);
R result = R.builder().code(200).msg("登录成功").info(token).build();
String json = JSONUtil.toJsonStr(result);
response.getWriter().write(json);
}
}
```
最后完成JWT结合Redis的token过滤器
```java
@Component
public class TokenFilter extends OncePerRequestFilter {
@Resource
private RedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
String requestURI = request.getRequestURI();
// 登录接口直接放行
if(requestURI.equals("/user/login")) {
filterChain.doFilter(request, response);
} else {
String token = request.getHeader("token");
if(!StringUtils.hasText(token)) {
R result = R.builder().code(901).msg("请求Token为空").build();
response.getWriter().write(JSONUtil.toJsonStr(result));
} else {
boolean verify = false;
try {
verify = JWTUtil.verify(token, Constant.SECRET.getBytes());
} catch(Exception e) {
e.printStackTrace();
}
if (!verify) {
R result = R.builder().code(902).msg("请求Token不合法").build();
response.getWriter().write(JSONUtil.toJsonStr(result));
} else {
JSONObject payloads = JWTUtil.parseToken(token).getPayloads();
String userJSON = payloads.get("user", String.class);
TUser tUser = JSONUtil.toBean(userJSON, TUser.class);
Integer userId = tUser.getId();
String redisToken = (String) redisTemplate.opsForHash().get(Constant.REDIS_TOKEN_KEY, String.valueOf(userId));
if(!token.equals(redisToken)) {
R result = R.builder().code(903).msg("请求Token错误").build();
response.getWriter().write(JSONUtil.toJsonStr(result));
} else {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(tUser, null, tUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
}
}
}
}
```