# 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(); } } ``` ## 最后完成页面 ![自定义登录页.png](pictura/%E8%87%AA%E5%AE%9A%E4%B9%89%E7%99%BB%E5%BD%95%E9%A1%B5.png) ## 获取用户登录身份 首先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 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 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 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); } } } } } } ```