spring security 认证授权详解

spring security简介

Spring Security 是 Spring家族中的一个安全管理框架,它提供了更丰富的功能做认证授权

  • 认证:当前用户有没有权限登录,是否为本系统用户
  • 授权:当前登录的用户有没有操作功能的权限

spring security的搭建

引入依赖

XML 复制代码
<dependencies>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.5.2</version>
        </dependency>

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

创建数据库表

sql 复制代码
create table user
(
    password varchar(100) null,
    username varchar(50)  null,
    id       varchar(50)  not null
        primary key
)
    comment '用户表';

项目配置如下

bash 复制代码
server:
  port: 9000
spring:
  datasource:
    url: jdbc:mysql://xxxx:3306/security_db?useSSL=true&serverTimezone=Asia/Shanghai
    username: root
    password: root123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    cluster:
      nodes:
        - 39.106.53.30:6379
        - 39.106.53.30:6380
        - 39.106.53.30:6381
        - 39.106.53.30:6382
        - 39.106.53.30:6383
        - 39.106.53.30:6384
mybatis-plus:
  mapper-locations:  classpath:/mapper/*.xml
  type-aliases-package: com.tech.security.securityservice.model

直接访问接口

没有经过认证的接口都会进入spring security的默认登录界面

spring security的原理

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器

主要的过滤器如下

  • UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
  • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。
  • FilterSecurityInterceptor:负责权限校验的过滤器

可以看到,在spring 容器中就有spring security的过滤器链,包括熟悉的过滤器UsernamePasswordAuthenticationFilter、FilterSecurityInterceptor等过滤器

spring security认证

认证流程

Authentication接口 : 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口 :定义了认证Authentication的方法
UserDetailsService接口 :加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回,然后将这些信息封装到Authentication对象中

UsernamePasswordAuthenticationFilter实现类 :实现了我们最常用的基于用户名和密码的认证逻辑,封装Authentication对象
DaoAuthenticationProvider实现类:是AuthenticationManager中管理的其中一个Provider,因为是要访问数据库

定义jwt认证工具类

我这里采用的是java-jwt,可能和jj-jwt写法和解析方式有所不同

java 复制代码
/**
 * @author sl
 */
@Slf4j
public class JwtTokenUtils {


    /**
     * 额外的数据,越复杂越安全
     */
    private static final String SING_VALUE = "dsdadfsghkjlsdfnmkjsd";

    /**
     * 过期时间
     */
    private static ReactiveRedisOperations<Object, Object> redisTemplate;


    public static String getJwtToken(String id){

        String JwtToken = JWT.create()
                .withClaim("userId",id)
                .withExpiresAt(DateUtil.offsetHour(new Date(),1))
                .sign(Algorithm.HMAC256(SING_VALUE));

        return JwtToken;
    }

    public static boolean CheckToken(String token){
        try {
            JWT.require(Algorithm.HMAC256(SING_VALUE)).build().verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    public static String parseTokenInfo(String token){
        DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING_VALUE)).build().verify(token);

        Claim claim = verify.getClaim("userId");

        return claim.asString();
    }
}

定义userDetails实现类

java 复制代码
@Data
public class LoginUser implements UserDetails {

    private String id;

    private String username;

    private String password;

    private String token;

    private String authority;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {return null;}

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

定义UserDetailsService实现类

目的是去数据库中查询用户信息,并返回UserDetails对象

java 复制代码
/**
 * @author sl
 */
@Service
public class SecurityUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("username",username);
        User user = userMapper.selectOne(userQueryWrapper);;

        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或密码错误");
        }

        //TODO 根据用户查询权限信息 添加到LoginUser中

        LoginUser loginUser = new LoginUser();

        loginUser.setId(user.getId());
        loginUser.setUsername(user.getUsername());
        loginUser.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
        loginUser.setAuthority("");
        return loginUser;
    }
}

登录以及退出登录

通过认证器进入登录认证流程,退出认证直接删除redis缓存即可

java 复制代码
@Service
public class UserServiceImpl implements UserService {


    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 认证器
     */
    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public LoginUser login(LoginUser user) {

        // 使用security认证
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        LoginUser principal = (LoginUser) authenticate.getPrincipal();
        if (authenticate.isAuthenticated()) {
            String jwtToken = JwtTokenUtils.getJwtToken(principal.getId());
            redisTemplate.opsForValue().set("login_"+principal.getId(), JSON.toJSONString(principal) ,1,TimeUnit.DAYS);
            principal.setToken(jwtToken);
        }

        return principal;
    }

    @Override
    public void logout() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        redisTemplate.delete("login:"+loginUser.getId());
    }

}

配置自定义认证过滤器进行jwt认证

主要是解析请求头中包含的token信息,并存入SecurityContextHolder上下文中

java 复制代码
/**
 * @author Administrator
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        String loginUserString = null;
        try {
            if (!StringUtils.hasText(token)) {
                //放行
                filterChain.doFilter(request, response);
                return;
            }

            if(token !=null){
                String userId= JwtTokenUtils.parseTokenInfo(token);

                loginUserString= redisTemplate.opsForValue().get("login_" + userId);
            }
        }catch (Exception e) {
            throw new RuntimeException("用户未登录");
        }

        LoginUser loginUser = JSON.parseObject(loginUserString, LoginUser.class);
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}

将jwt认证过滤器加入过滤器链中

java 复制代码
/**
 * @author sl
 */
@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable();
        httpSecurity.authorizeRequests()
                .antMatchers("/user/login").anonymous()
                .anyRequest().authenticated()
                .and()
                .formLogin();
        // 在UsernamePasswordAuthenticationFilter之前添加自定义token前置过滤器
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        return httpSecurity.build();
    }

}

测试认证

直接访问登录接口,传递用户名密码进入认证流程, 返回token

携带token访问,认证成功

spring security授权

授权的概念:当前登录的用户有没有操作功能的权限

授权的模型:rbac(Role-Based Access Control)模型

授权方式:注解,配置

rbac授权模型

基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型

数据库脚本

共5张表结构,用户表,角色表,权限表,用户角色表,用户权限表

sql 复制代码
create table perm
(
    id          varchar(60)                not null
        primary key,
    func_name   varchar(64) default 'NULL' not null comment '功能名称',
    path        varchar(200)               null comment '路由地址',
    status      char        default '0'    null comment '功能状态(0正常 1停用)',
    perms       varchar(100)               null comment '权限标识',
    create_time datetime                   null,
    update_time datetime                   null,
    del_flag    int         default 0      null comment '是否删除(0未删除 1已删除)',
    remark      varchar(500)               null comment '备注'
)
    comment '权限表';

INSERT INTO perm (id, func_name, path, status, perms, create_time, update_time, del_flag, remark) VALUES ('dfasdfgfdg', '审批功能', '/audit', '0', 'audit', '2023-10-11 15:20:16', '2023-10-11 15:20:18', 0, '审批功能');
INSERT INTO perm (id, func_name, path, status, perms, create_time, update_time, del_flag, remark) VALUES ('fdsfdsfhgfsjy', '删除功能', '/delete', '0', 'delete', '2023-10-11 11:26:58', '2023-10-11 11:27:02', 0, '删除权限');


create table user
(
    id       varchar(50)  not null
        primary key,
    username varchar(50)  null,
    password varchar(100) null
)
    comment '用户表';

INSERT INTO user (id, username, password) VALUES ('qwertreyt', 'ceshi', 'ceshi');
INSERT INTO user (id, username, password) VALUES ('sdafasf214', 'admin', 'admin');

create table role
(
    id          varchar(60)      not null
        primary key,
    name        varchar(128)     null,
    role_key    varchar(100)     null comment '角色权限字符串',
    status      char default '0' null comment '角色状态(0正常 1停用)',
    del_flag    int  default 0   null comment 'del_flag',
    create_time datetime         null,
    update_time datetime         null,
    remark      varchar(500)     null comment '备注'
)
    comment '角色表';
	
INSERT INTO role (id, name, role_key, status, del_flag, create_time, update_time, remark) VALUES ('qwerqwerer', '管理员', 'amdin', '0', 0, '2023-10-11 11:22:00', '2023-10-11 11:22:09', '管理员角色');
INSERT INTO role (id, name, role_key, status, del_flag, create_time, update_time, remark) VALUES ('uiplpptry', '普通用户', 'ceshi1', '0', 0, '2023-10-11 15:31:39', '2023-10-11 15:31:42', '普通用户');

create table role_perm
(
    role_id varchar(60) not null comment '角色ID',
    perm_id varchar(60) not null comment '菜单id',
    primary key (role_id, perm_id)
);

INSERT INTO role_perm (role_id, perm_id) VALUES ('qwerqwerer', 'dfasdfgfdg');
INSERT INTO role_perm (role_id, perm_id) VALUES ('qwerqwerer', 'fdsfdsfhgfsjy');
INSERT INTO role_perm (role_id, perm_id) VALUES ('uiplpptry', 'fdsfdsfhgfsjy');

create table user_role
(
    user_id varchar(60) not null comment '用户id',
    role_id varchar(60) not null comment '角色id',
    primary key (user_id, role_id)
);

INSERT INTO user_role (user_id, role_id) VALUES ('sdafasf214', 'qwerqwerer');
INSERT INTO user_role (user_id, role_id) VALUES ('qwertreyt', 'uiplpptry');

开启权限配置

@EnableGlobalMethodSecurity(prePostEnabled = true)

java 复制代码
/**
 * @author sl
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig {

    ....

}

建立mapper通过用户id查询权限

XML 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.tech.security.securityservice.mapper.PermMapper">
    <select id="findPermByUserId" resultType="perm">
        select p.*
        from
            user u left join user_role ur
                             on ur.user_id = u.id
                   left join  role_perm rm
                              on ur.role_id = rm.role_id
                   left join perm p
                             on rm.perm_id = p.id
        where user_id  = #{userId}
    </select>
</mapper>

将用户权限封装到userDetails对象中

LoginUser加入权限

java 复制代码
package com.tech.security.securityservice.model;

import com.alibaba.fastjson.annotation.JSONField;
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.List;
import java.util.stream.Collectors;

@Data
public class LoginUser implements UserDetails {

    private String id;

    private String username;

    private String password;

    private String token;

    /**
     *存储权限信息
     */
    private List<String> permissions;

    /**
     *存储SpringSecurity所需要的权限信息的集合
     */
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities!=null){
            return authorities;
        }
        //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
        authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

封装权限到userDetails对象中

java 复制代码
/**
 * @author sl
 */
@Service
public class SecurityUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private PermMapper permMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("username",username);
        User user = userMapper.selectOne(userQueryWrapper);;

        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或密码错误");
        }

        LoginUser loginUser = new LoginUser();
        loginUser.setId(user.getId());
        loginUser.setUsername(user.getUsername());
        loginUser.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
        //根据用户查询权限信息 添加到LoginUser中
        List<Perm> perms= permMapper.findPermByUserId(user.getId());
        List<String> permsList = perms.stream().map(s -> s.getPerms()).collect(Collectors.toList());
        loginUser.setPermissions(permsList);
        return loginUser;
    }
}

别忘记在自定义认证器中加入权限进入上下文中

java 复制代码
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

编写controller加权限注解测试

hasAuthority代表拥有该权限可以访问,还有hasRole等,默认会拼接上ROLE_,在这里不做过多介绍,@PreAuthorize("@myAccess.hasAuthority('audit')"),myAccess为自定义权限校验

java 复制代码
/**
 * @author sl
 */
@RestController
public class TestSecurityAccessController {

    /**
     * 测试数据访问接口
     * @return
     */
    @GetMapping("/access")
    @PreAuthorize("hasAuthority('access')")
    public String access(){
        return "access success!!";
    }

    /**
     *  @PreAuthorize("@myAccess.hasAuthority('audit')")
     *  使用自定义权限校验
     * @return
     */
    @GetMapping("/audit")
    @PreAuthorize("@myAccess.hasAuthority('audit')")
    public String audit(){
        return "audit access!!";
    }

    /**
     * 使用spring security权限校验
     * @return
     */
    @GetMapping("/delete")
    @PreAuthorize("hasAuthority('delete')")
    public String delete(){
        return "delete access!!";
    }
}

基于配置的权限配置

java 复制代码
        httpSecurity.authorizeRequests()
                .antMatchers("/user/login").anonymous()
                // 基于配置的权限处理
                .antMatchers("/audit").hasAuthority("audit")
                .anyRequest().authenticated()
                .and()
                .formLogin();

自定义权限校验

在controller中@PreAuthorize("@myAccess.hasAuthority('audit')") 就使用了自定义的权限校验

java 复制代码
/**
 * @author sl
 * 自定义权限校验
 */
@Component("myAccess")
public class MyAccessHandler {

    public boolean hasAuthority(String authority){
        //获取当前用户的权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        //判断用户权限集合中是否存在authority
        return permissions.contains(authority);
    }
}

启动项目开始权限测试

登录测试账户

当前登录的账户ceshi登录成功,当前测试账户具有删除权限

访问无权限的审计功能

当前访问为403,无授权的http状态,无法访问

访问删除功能

访问成功,代表用户具有的删除权限可以访问,授权成功

认证授权优化问题

统一返回结果,自定义异常处理器

当前的认证授权,如果失败了返回的结果不是很友好,我们希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,方便统一结构

spring security的异常处理

  • 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
  • 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
  • 所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

工具类WebUtils

java 复制代码
public class WebUtils {
    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string   待渲染的字符串
     */
    public static void renderString(HttpServletResponse response, String string) {
        try {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

认证失败处理器

java 复制代码
/**
 * @author sl
 * 认证失败返回处理
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        Result result = new Result(HttpStatus.UNAUTHORIZED.value(), "用户认证失败,请重新登录");
        String json = JSON.toJSONString(result);
        // 处理异常,调用工具类
        WebUtils.renderString(response, json);
    }
    
}

授权失败处理器

java 复制代码
/**
 * @author sl
 * 权限校验失败处理
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Result result = new Result(HttpStatus.FORBIDDEN.value(), "权限不足");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);
    }
}

将授权以及认证处理器配置给SpringSecurity

java 复制代码
/**
 * @author sl
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable();
        httpSecurity.authorizeRequests()
                .antMatchers("/user/login").anonymous()
                // 基于配置的权限处理
//                .antMatchers("/audit").hasAuthority("audit")
                .anyRequest().authenticated()
                .and()
                .formLogin();
        // 在UsernamePasswordAuthenticationFilter之前添加自定义token前置过滤器
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 自定义认证失败和权限处理失败处理器
        httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);
        // 允许跨越
        httpSecurity.cors();
        return httpSecurity.build();
    }

}

测试认证失败返回和授权失败返回

未登录显示认证失败

登录之后返回权限不足

项目gitee地址

https://gitee.com/watcherman/security-service.git

相关推荐
JH3073几秒前
Oracle与MySQL中CONCAT()函数的使用差异
数据库·mysql·oracle
蓝染-惣右介2 分钟前
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
java·数据库·tomcat·mybatis
小林想被监督学习3 分钟前
idea怎么打开两个窗口,运行两个项目
java·ide·intellij-idea
冷心笑看丽美人4 分钟前
Spring框架特性及包下载(Java EE 学习笔记04)
数据库
HoneyMoose5 分钟前
IDEA 2024.3 版本更新主要功能介绍
java·ide·intellij-idea
我只会发热7 分钟前
Java SE 与 Java EE:基础与进阶的探索之旅
java·开发语言·java-ee
是老余8 分钟前
本地可运行,jar包运行错误【解决实例】:通过IDEA的maven package打包多模块项目
java·maven·intellij-idea·jar
crazy_wsp9 分钟前
IDEA怎么定位java类所用maven依赖版本及引用位置
java·maven·intellij-idea
.Ayang11 分钟前
tomcat 后台部署 war 包 getshell
java·计算机网络·安全·web安全·网络安全·tomcat·网络攻击模型
一直学习永不止步16 分钟前
LeetCode题练习与总结:最长回文串--409
java·数据结构·算法·leetcode·字符串·贪心·哈希表