课程项目设计--spring security--认证管理功能--宿舍管理系统--springboot后端

写在前面:
还要实习,每次时间好少呀,进度会比较慢一点
本文主要实现是用户管理相关功能。
前文项目建立

验证码功能

验证码采用的是hutool工具的验证码
hutool官方地址

工具模板采用有来开源组织

验证码配置

yml配置

CaptchaConfig:
  #  验证码缓存过期时间(单位:秒)
  ttl: 120l
  # 验证码内容长度
  length: 4
  # 验证码宽度
  width: 120
  # 验证码高度
  height: 40
  # 验证码字体
  font-name: Verdana
  # 验证码字体大小
  fontSize: 20

配置类

/**
 * EasyCaptcha 配置类
 * 
 * @author haoxr
 * @since 2023/03/24
 */
@ConfigurationProperties(prefix = "easy-captcha")
@Configuration
@Data
public class CaptchaConfig {

    // 验证码类型
    private CaptchaTypeEnum type = CaptchaTypeEnum.ARITHMETIC;

    // 验证码缓存过期时间(单位:秒)
    @Value("${captcha.ttl}")
    private long ttl;

    // 内容长度
    @Value("${captcha.length}")
    private int length;
    // 宽度
    @Value("${captcha.width}")
    private int width;
    // 验证码高度
    @Value("${captcha.height}")
    private int height;

    // 验证码字体
    @Value("${captcha.font-name}")
    private String fontName;

    // 字体风格
    private Integer fontStyle = Font.PLAIN;

    // 字体大小
    @Value("${captcha.font-size}")
    private int fontSize;

}

验证码生成工具类

@Component
@RequiredArgsConstructor
public class EasyCaptchaProducer {
    private final CaptchaConfig captchaConfig;

    public Captcha getCaptcha() {
        Captcha captcha;
        int width = captchaConfig.getWidth();
        int height = captchaConfig.getHeight();
        int length = captchaConfig.getLength();
        String fontName = captchaConfig.getFontName();

        switch (captchaConfig.getType()) {
            case ARITHMETIC -> {
                captcha = new ArithmeticCaptcha(width, height);
                captcha.setLen(2);
            }
            case CHINESE -> {
                captcha = new ChineseCaptcha(width, height);
                captcha.setLen(length);
            }
            case CHINESE_GIF -> {
                captcha = new ChineseGifCaptcha(width, height);
                captcha.setLen(length);
            }
            case GIF -> {
                captcha = new GifCaptcha(width, height);//最后一位是位数
                captcha.setLen(length);
            }
            case SPEC -> {
                captcha = new SpecCaptcha(width, height);
                captcha.setLen(length);
            }
            default -> throw new RuntimeException("验证码配置信息错误!正确配置查看 CaptchaTypeEnum ");
        }
        captcha.setFont(new Font(fontName, captchaConfig.getFontStyle(), captchaConfig.getFontSize()));
        return captcha;
    }


}

添加依赖

        <!-- Java8 之后JavaScript引擎nashorn被移除导致验证码解析报错-->
        <dependency>
            <groupId>org.openjdk.nashorn</groupId>
            <artifactId>nashorn-core</artifactId>
            <version>${nashorn.version}</version>
        </dependency>

功能测试

        Captcha captcha = easyCaptchaProducer.getCaptcha();
        try (OutputStream ops = new FileOutputStream("d://captcha.jpg")) {
            captcha.out(ops);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(captcha.text());

测试结果
在这里插入图片描述
在这里插入图片描述

编写controller接口

@Tag(name = "01-认证中心")
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthController {

    private final EasyCaptchaService easyCaptchaService;

    @Operation(summary = "获取验证码")
    @GetMapping("/captcha")
    public Result<CaptchaResult> getCaptcha() {
        CaptchaResult captcha = easyCaptchaService.getCaptcha();
        return Result.success(captcha);
    }
}

启动项目

记住这里,这是你spring security 的密码
在这里插入图片描述

生成http

通过base64转图片的在线工具可以看到
在这里插入图片描述
说明编写成功了。

security配置

在上面我们默认的是spring security 自动的密码。我们现在需要自己设置密码。

spring security 框架捏,不太好说这玩意。挺忘记了。
不过spring boot3使用的是spring security6.0版本和以前的有很大差别,6.0通过配置bean来进行。所以也还好,反正都是从头学。
首先需要配置security的配置类

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {


    // 密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    /**
     * 不走过滤器链的放行配置
     * 默认放行静态资源、登录接口、验证码接口、Swagger接口文档
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring()
                .requestMatchers(
                        "/auth/captcha",
                        "/webjars/**",
                        "/doc.html",
                        "/swagger-resources/**",
                        "/swagger-ui/**",
                        "/ws/**"
                );
    }
}
    /**
     * 认证管理器
     *
     * @param authenticationConfiguration 认证配置
     * @return 认证管理器
     * @throws Exception 异常
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(requestMatcherRegistry ->
                        requestMatcherRegistry.requestMatchers(SecurityConstants.LOGIN_PATH).permitAll()
                                .anyRequest().authenticated())
                .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
                        httpSecurityExceptionHandlingConfigurer
                                .authenticationEntryPoint(authenticationEntryPoint)
                                .accessDeniedHandler(accessDeniedHandler))
                .csrf(AbstractHttpConfigurer::disable);

        // 验证码校验过滤器
        http.addFilterBefore(new VerifyCodeFilter(), UsernamePasswordAuthenticationFilter.class);
        // JWT 校验过滤器
        http.addFilterBefore(new JwtAuthenticationFilter(jwtTokenManager), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

这里还用到了2个拦截器

拦截器配置

验证码拦截器

需求:对登录请求进行拦截,如果是登录则需要先校验验证码是否正常,如果正确则放行。其他请求则直接放行。

public class VerifyCodeFilter extends OncePerRequestFilter {
    private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, "POST");

    public static final String VERIFY_CODE_PARAM_KEY = "verifyCode";
    public static final String VERIFY_CODE_KEY_PARAM_KEY = "verifyCodeKey";
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 如果是登录请求则校验验证码
        if (LOGIN_PATH_REQUEST_MATCHER.matches(request)){
            String code = request.getParameter(VERIFY_CODE_PARAM_KEY);
            String verifyCodeKey = request.getParameter(VERIFY_CODE_KEY_PARAM_KEY);

            // 由于这个不是bean,不能通过注入的方式获取,所以通过SpringUtil工具类获取
            RedisTemplate redisTemplate = SpringUtil.getBean("redisTemplate", RedisTemplate.class);
            String cacheCode =  Convert.toStr(redisTemplate.opsForValue().get(SecurityConstants.VERIFY_CODE_CACHE_PREFIX + verifyCodeKey));
            if (cacheCode == null) {
                // 验证码过期
                ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_TIMEOUT);
                return;
            }
            if (!StrUtil.equals(cacheCode,code)) {
                // 验证码错误
                ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_ERROR);
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
}

jwt拦截器

需求:处理登录请求以外的请求,每次需要验证jwt令牌,如果没问题则在该线程请求附加权限身份。

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, "POST");

    private final JwtTokenManager tokenManager;
    public JwtAuthenticationFilter(JwtTokenManager jwtTokenManager) {
        this.tokenManager = jwtTokenManager;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        if (!LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
            String jwt = RequestUtils.resolveToken(request);
            if (StringUtils.hasText(jwt) && SecurityContextHolder.getContext().getAuthentication() == null) {
                try {
                    Claims claims = this.tokenManager.parseAndValidateToken(jwt);
                    Authentication authentication = this.tokenManager.getAuthentication(claims);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                } catch (Exception e) {
                    ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
                }
            } else {
                ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
            }
        }
        chain.doFilter(request, response);
    }
}

思考

这2个拦截器一个需要登录一个除去登录,那么是不是可以放到一个拦截器里面去。各走各的。这样也明确一点。也不用2个拦截器找了。

如果改了记得改securityFilterChain

用户登录

需求:输入用户名和密码,验证用户身份。
需要写一个类继承UserDetails
在这里插入图片描述

另一个实现类继承SysUserService(SysUserDetailsService)
在这里插入图片描述
这2个一个是存储一个是查询。然后会自动和输入的username以及password进行比对
验证流程后面总结一个spring security的文。

SysUserDetailsService作用是查询该用户名的角色信息并返回UserDetails。

查询,调用SysUserService根据用户名查询所有的
在这里插入图片描述
由于认证信息需要角色信息和权限所以我们需要联表查询角色信息。
在依据角色信息查询权限。

        select u.id userId,
               u.name username,
               u.password,
               u.role,
               u.avatar,
               u.email,
               u.status,
               r.code
        from sys_user u
                 left join sys_user_role sur on u.id = sur.user_id
                 left join sys_role r on sur.role_id = r.id
        where u.name = #{username}
          AND u.deleted = 0

然后在依据角色查询权限
不过我感觉这个type硬编码挺严重的,也算学习一下这种mybatis里面枚举了。
如果没用角色则m.id = -1让其没权限。

<select id="listRolePerms" resultType="java.lang.String">
        select distinct m.perm
        from sys_menu m
        inner join sys_role_menu rm on m.id = rm.menu_id
        inner join sys_role r on r.id = rm.role_id
        where m.type = '${@com.yu.common.enums.MenuTypeEnum@BUTTON.getValue()}'
        and m.perm is not null
        <choose>
            <when test="roles!=null and roles.size()>0">
                and r.code in
                <foreach collection="roles" item="role" open="(" close=")" separator=",">
                    #{role}
                </foreach>
            </when>
            <otherwise>
                and m.id = -1
            </otherwise>
        </choose>
    </select>

controller验证,很明确的流程就是封装输入的,然后进行验证。失败了会报错返回。
成功则生成token将权限放入redis,将角色,用户名,id封装进jwt
然后进行返回。接下来查看jwtTokenManager.createToken

    @Operation(summary = "登录")
    @PostMapping("/login")
    public Result<LoginResult> login(
            @Parameter(description = "用户名", example = "admin") @RequestParam String username,
            @Parameter(description = "密码", example = "123456") @RequestParam String password
    ) {
        // 存储username和password
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                username.toLowerCase().trim(),
                password
        );
        // 验证用户名和密码
        Authentication authentication = authenticationManager.authenticate(authenticationToken);
        // 生成token
        String accessToken = jwtTokenManager.createToken(authentication);
        // 返回token
        LoginResult loginResult = LoginResult.builder()
                .tokenType("Bearer")
                .accessToken(accessToken)
                .build();
        return Result.success(loginResult);
    }
    @Schema(description ="登录响应对象")
    @Builder
    public static record LoginResult(
            @Schema(description = "访问token")
            String accessToken,

            @Schema(description = "token 类型",example = "Bearer")
            String tokenType,

            @Schema(description = "刷新token")
            String refreshToken,

            @Schema(description = "过期时间(单位:毫秒)")
            Long expires
    ) {
    }

jwt管理

采用hutool工具包进行jwt管理,以前用过java-jwt的,这次试试hutool。

    /**
     * 创建token
     *
     * @param authentication auth info
     * @return token
     */
    public String createToken(Authentication authentication) {
        SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();

        // 角色放入JWT的claims
        Set<String> roles = userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority).collect(Collectors.toSet());

        // 权限数据多放入Redis
        Set<String> perms = userDetails.getPerms();
        redisTemplate.opsForValue().set(SecurityConstants.USER_PERMS_CACHE_PREFIX + userDetails.getUserId(), perms);

        Map<String, Object> claims = Map.of(
                JWTPayload.ISSUED_AT, DateTime.now(),
                JWTPayload.EXPIRES_AT, DateTime.now().offset(DateField.SECOND, tokenTtl),
                "jti", IdUtil.fastSimpleUUID(),
                "userId", userDetails.getUserId(),
                "username", userDetails.getUsername(),
                "authorities", roles);

        return JWTUtil.createToken(claims, getSecretKeyBytes());
    }

验证

http测试:
之前测试挺头疼的。
需要先发送验证码的。
然后去base64转图片(后面直接打印了结果了)
进行测试
在这里插入图片描述
成功
在这里插入图片描述
后面去vue3前端测了。用的是有来开源vue3-element-admin修改。
成功了!
在这里插入图片描述

用户注销

从jwt中获取我们设置的jti唯一表示
然后需要将redis中的删除就可以了

    @Operation(summary = "注销", security = {@SecurityRequirement(name = SecurityConstants.TOKEN_KEY)})
    @DeleteMapping("/logout")
    public Result<String> logout(HttpServletRequest request) {
        String token = RequestUtils.resolveToken(request);
        if (StrUtil.isNotBlank(token)) {
            Claims claims = jwtTokenManager.getTokenClaims(token);
            String jti = StrUtil.toString(claims.getClaim("jti"));

            Date expiration = jwtTokenManager.getExpiration(claims);
            if (expiration != null) {
                // 有过期时间,在token有效时间内存入黑名单,超出时间移除黑名单节省内存占用
                long ttl = (expiration.getTime() - System.currentTimeMillis());
                redisTemplate.opsForValue().set(SecurityConstants.BLACK_TOKEN_CACHE_PREFIX + jti, null, ttl, TimeUnit.MILLISECONDS);
            } else {
                // 无过期时间,永久加入黑名单
                redisTemplate.opsForValue().set(SecurityConstants.BLACK_TOKEN_CACHE_PREFIX + jti, null);
            }
        }
        SecurityContextHolder.clearContext();
        return Result.success("注销成功");
    }

流程小结

验证码

获取随机验证码

  • 验证码接口放行,无视security
  • 存放redis用,key = SecurityConstants.VERIFY_CODE_CACHE_PREFIX +verifyCodeKey(生成)

验证验证码

  • 拦截登录请求
  • 查询redis
    • 如果null,则过期
    • 如果错误,则返回
    • 正确放行

jwt令牌管理

  • 拦截所有除了登录的请求
  • 从jwt中解析获取Authentication
  • 放入线程中

登录

  • 框架校验

    • 获取认证信息,依据user和role表获取角色基本信息和角色
    • 依据角色获取权限
    • Authentication存放id,用户名,密码,是否启用,权限,角色,数据权限
  • 依据Authentication生成jwt

    • 存放jti随机id,userid,用户名,角色信息,权限数据
    • 过期时间5小时

注销

  • 拉黑jwt的jti
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 外卖项目的前后端分离是指将项目的前端部分和后端部分进行分离开发,前端使用Vue框架,后端使用Spring Boot框架。 前端使用Vue框架可以提供良好的用户界面和交互体验。Vue具有组件化的特点,使得前端开发更加模块化和可维护。同时,Vue的数据绑定和响应式设计可以帮助实现快速更新页面的功能。通过Vue,用户可以方便地浏览外卖项目的菜单、下单、支付等操作,提升用户的使用体验。 后端使用Spring Boot框架可以提供强大的后台支持。Spring Boot是一种轻量级的Java框架,可以快速搭建和部署项目,减少开发的复杂度。使用Spring Boot,开发人员可以方便地实现外卖项目的后台逻辑,例如订单的处理、菜单的管理、支付的接口等。同时,Spring Boot集成了许多常用且可靠的开源库,为项目提供了高效、稳定的基础设施。 前后端分离的优势在于前端和后端可以并行开发,提高开发效率。前端和后端之间通过定义接口进行通信,降低了耦合度,灵活性更强。同时,单独部署前端和后端也可以提高项目的可维护性和可扩展性。例如,当需要添加新的功能或修改现有功能时,只需要修改相应的前端或后端代码,而不会影响到整个项目。 总之,外卖项目的前后端分离以及使用Vue和Spring Boot框架的设计选择,可以帮助实现一个高效、稳定、可扩展的外卖平台。 ### 回答2: 外卖项目采用前后端分离的架构,前端使用Vue框架,后端采用Spring Boot框架。 前端使用Vue框架可以实现用户界面的可视化设计和交互体验。Vue框架具有简单易用、灵活可扩展、高效性能等特点,适用于构建复杂的单页面应用(SPA)。通过Vue框架,可以实现用户注册、登录、浏览菜单、购物车管理、订单处理等功能的前端设计和开发。前端通过调用后端接口,获取后端处理的数据,并将数据展示在用户界面上。 后端使用Spring Boot框架可以实现业务逻辑的处理和数据存储。Spring Boot框架提供了快速构建、简化配置和集成多种功能的特性,适用于快速开发和维护可靠的应用程序。通过Spring Boot框架,可以处理用户注册、登录验证、菜单管理、订单处理等业务逻辑,并与数据库进行交互,存储与外卖项目相关的数据。后端还需要提供RESTful接口,供前端调用和交互。 前后端分离架构的好处是可以实现前端与后端的解耦,提高开发效率和维护性。前端和后端可以同时进行开发,并可采用不同的技术栈,使得团队成员能够专注于自己的领域。前后端分离还可以实现多端复用,例如可以用同一组后端接口提供给Web端和移动端调用。 总的来说,外卖项目采用前后端分离的架构,借助Vue和Spring Boot框架实现了用户界面的展示和交互以及业务逻辑的处理和数据存储,从而使得项目开发更加高效和可维护。 ### 回答3: 外卖项目采用前后端分离的架构,前端使用Vue.js框架进行开发,后端使用Spring Boot框架进行开发。 前端使用Vue.js框架的原因是因为Vue.js具有简洁、高效、灵活的特点,能够轻松构建交互式的用户界面。Vue.js还拥有一套完整的生态系统,能够方便地进行组件化开发,并提供了强大的工具来处理数据和状态的变化。 后端使用Spring Boot框架的原因是因为Spring Boot是一个简化了Spring开发的微框架,能够快速构建可独立运行的、生产级的应用。Spring Boot提供了大量的开箱即用的特性,如自动配置、快速开发等,能够极大地提高开发效率。 在外卖项目中,前端负责用户界面的展示和交互逻辑的实现。前端通过Vue.js进行组件化开发,将页面拆分为多个可复用的组件,提高开发效率和代码维护性。前端还通过Vue.js提供的路由功能,实现不同页面之间的跳转和导航。同时,前端还与后端通过HTTP协议进行通信,获取后端提供的数据和服务,并将其展示给用户。 后端负责处理前端发送的请求,并根据业务逻辑进行相应的处理,最终返回数据给前端。后端还负责与数据库交互,对数据进行增删改查操作。后端使用Spring Boot提供的RESTful风格的API,能够轻松构建出符合规范的接口。同时,后端还可以利用Spring Security进行权限管理,确保只有具备相应权限的用户才能访问特定的接口。 综上所述,外卖项目采用前后端分离的架构,前端使用Vue.js框架进行开发,后端使用Spring Boot框架进行开发,能够提高开发效率和代码的可维护性,同时还能够满足用户对于界面交互和数据操作的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只小余

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值