springboot第22集:security,Lombok,token,redis

Spring Security是一个基于Spring框架的权限管理框架,用于帮助应用程序实现身份验证和授权功能。它可以为Web应用程序、REST API和方法级安全性提供支持,并支持各种认证方式。

Spring Security最初是Acegi Security的前身,但由于其配置繁琐而受到批评。随着Spring Boot的出现,Spring Security的易用性得到了极大的提高,成为了Spring Boot和Spring Cloud项目中常用的安全框架。

Spring Security的基本功能包括认证和授权。认证方面,它支持多种常见的认证方式,例如基于表单的认证、HTTP基本认证、OpenID Connect、OAuth2等。授权方面,它提供了基于URL的请求授权、支持方法访问授权以及对象访问授权等能力,可用于限制用户对应用程序中资源的访问。除此之外,Spring Security还提供了一些其他的安全特性,例如CSRF防护、会话管理等,以帮助应用程序保护安全性和保密性。

Spring Security是一个强大的安全性框架,它被广泛用于基于Java的Web应用程序中。它基于Servlet过滤器实现了一套标准化的认证和授权机制,通过一系列Filter来处理Web请求,以确保只有经过身份验证的用户可以访问系统中的受保护资源。

在Spring Security中,Filter链是一个重要的概念。它由多个Filter组成,每个Filter都负责执行不同的任务,例如身份验证、授权、防止CSRF攻击等。这些Filter按照特定的顺序组成了一个链条,每当一个请求到达应用程序时,请求将被传递给Filter链,直到找到合适的Filter进行处理或者抛出异常。

在Filter链中,认证和授权通常是最核心的部分。Spring Security提供了各种方式来进行身份验证和授权,例如表单登录、基本认证、OAuth2等。在处理过程中,如果出现任何异常,如认证失败或权限不足,Spring Security将会抛出异常并将其传递给异常处理器进行处理。异常处理器通常会捕获异常、记录日志并向用户显示错误消息,以便及时解决问题。

总之,Filter链是Spring Security中非常重要的一环,它能够为我们的Web应用程序提供强大的安全性保障。通过组织不同的Filter,Spring Security可以提供多种不同的身份验证和授权机制,使我们能够轻松地保护应用程序中的敏感资源。

除了上述提到的Spring Security常用组件外,还有以下一些组件:

  1. AccessDecisionManager:用于根据用户和资源的相关信息判断是否允许用户访问资源。
  2. AuthenticationEntryPoint:如果一个未认证的用户试图访问需要认证的资源,会被重定向到该接口实现的方法处理。
  3. AuthenticationProvider:用于对用户进行认证并生成认证对象 Authentication。
  4. FilterSecurityInterceptor:在请求到达后台之前进行拦截和处理,包含很多安全检查点。
  5. RememberMeAuthenticationProvider:为支持“记住我”功能提供的认证处理器,用于生成认证对象 Authentication。
  6. SessionRegistry:用于跟踪已经登录的用户,通常在实现“单点登录”时使用。

这些组件可以通过配置文件中的bean来进行自定义,并且可以根据具体情况进行组合搭配,以实现更加灵活、高效的安全管理方案。

引入 Spring Security 依赖

代码语言:javascript
复制
<!--引入 Spring Security-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

引入依赖后,不做任何配置,Spring Security 会自动生效,请求将跳转登录页面

默认用户名、密码和权限可在 application.yaml 中配置

代码语言:javascript
复制
@Configuration
@EnableWebSecurity
// 开启注解设置权限
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    // 配置密码加密器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 配置认证管理器
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password(passwordEncoder().encode("123")).roles("admin")
                .and()
                .withUser("user")
                .password(passwordEncoder().encode("456")).roles("user");
    }
    
    // 配置安全策略
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 设置路径及要求的权限,支持 ant 风格路径写法
        http.authorizeRequests()
            // 设置 OPTIONS 尝试请求直接通过
             .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
             .antMatchers("/api/demo/user").hasAnyRole("user", "admin")
             // 注意使用 hasAnyAuthority 角色需要以 ROLE_ 开头
                .antMatchers("/api/demo/admin").hasAnyAuthority("ROLE_admin")
                .antMatchers("/api/demo/hello").permitAll()
                .and()
             // 开启表单登录
                .formLogin().permitAll()
                .and()
             // 开启注销
                .logout().permitAll();
    }
}

代码语言:javascript
复制
@Override
protected void configure(HttpSecurity http) throws Exception {
    // 关闭 csrf 防御
    http.csrf().disable();
    // 关闭会话管理
    http.sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    // ...
}
代码语言:javascript
复制
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        // 判断是否为 JSON 格式请求
        if(request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
            // ...
        } else {
            return super.attemptAuthentication(request, response);
        }
    }
}
代码语言:javascript
复制
@Autowired
private CustomUserDetailsService customUserDetailsService
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(customUserDetailsService)
        .passwordEncoder(passwordEncoder());
}
代码语言:javascript
复制
public class CustomUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        // 根据 username 查询用户
        User user = userMapper.getUserByUsername(s);
        if (user == null) {
            // ...
        }
        // 查询角色或权限
        List<SimpleGrantedAuthority> authorities = userMapper.listRolesByUsername(s)
            .stream()
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());
        // 构造 UserDetails 实例并返回
    }
}
代码语言:javascript
复制
public class CustomUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        // 根据 username 查询用户
        User user = userMapper.getUserByUsername(s);
        if (user == null) {
            // ...
        }
        // 查询角色或权限
        List<SimpleGrantedAuthority> authorities = userMapper.listRolesByUsername(s)
            .stream()
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());
        // 构造 UserDetails 实例并返回
    }
}
代码语言:javascript
复制
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin().permitAll()
        .loginProcessingUrl("/login")
        .successHandler(customLoginSuccessHandler)
}

CustomLoginSuccessHandler,以 JSON 形式返回前端,携带生成的 Token

代码语言:javascript
复制
@Component
@RequiredArgsConstructor
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {

    private final JwtUtil jwtUtil;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        // 构造一个统一返回格式对象
        Map<String, Object> res = new HashMap<>();
        res.put("code", 200);
        res.put("message": "认证成功");
        res.put("path": "login");
        Object principal = authentication.getPrincipal();
        if (principal instanceof User) {
            // 根据用户信息,使用 JWT 工具类构建 Token
            // ...
            // 存到返回内容中
            res.put("data", "xxxxxx")
        }
        // 以 JSON 格式写入 response
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        PrintWriter writer = response.getWriter();
        writer.print(JsonUtil.Obj2Str(res));
        writer.flush();
    }
}

代码语言:javascript
复制
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin().permitAll()
        .loginProcessingUrl("/login")
        .failureHandler(customLoginFailureHandler)
}
代码语言:javascript
复制
@Component
public class CustomLoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) {
        // 封装的统一返回格式对象
        Res<Object> res = Res.of(ResCode.TOKEN_CREATE_FAIL).path("/login");
        // 根据异常设置失败信息
        if (exception instanceof LockedException) {
            res.errorMsg("账户被锁定");
        } else if (exception instanceof CredentialsExpiredException) {
            res.errorMsg("密码过期");
        } else if (exception instanceof AccountExpiredException) {
            res.errorMsg("账户过期");
        } else if (exception instanceof DisabledException) {
            res.errorMsg("账户被禁用");
        } else if (exception instanceof BadCredentialsException) {
            res.errorMsg("用户名或者密码输入错误");
        }
        // 封装的 JSON 格式写入 response 工具方法
        WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res));
    }
}
代码语言:javascript
复制
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.exceptionHandling()
        .authenticationEntryPoint(customAuthenticationEntryPoint)
}
代码语言:javascript
复制
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, 
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        // 构造未登录的返回内容
        Res<Object> res = Res.of(ResCode.TOKEN_NOT_EXIST)
                .path(request.getRequestURI());
        WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res));
    }
}
代码语言:javascript
复制
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.exceptionHandling()
        .accessDeniedHandler(customAccessDeniedHandler);
}
代码语言:javascript
复制
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 构造权限不足的返回内容 
        Res<Object> res = Res.of(ResCode.TOKEN_NO_AUTHORITY)
                .path(request.getRequestURI());
        WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res));
    }
}
代码语言:javascript
复制
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.logout().permitAll()
        .logoutUrl("/logout")
        .logoutSuccessHandler(logoutSuccessHandler);
}
代码语言:javascript
复制
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                Authentication authentication) throws IOException, ServletException {
        // 构造注销成功的返回内容
        Res<String> res = Res.ok("注销成功").path("/logout");
        WebUtil.writeJsonToResponse(response, JsonUtil.objToStr(res));
    }
}
代码语言:javascript
复制
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(jwtAuthenticationTokenFilter,
                         UsernamePasswordAuthenticationFilter.class);
}
代码语言:javascript
复制
@Component
@RequiredArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private final UserDetailsService userDetailsService;
    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, 
                                    HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {
        // 取出 header 中的 token 进行校验
        String authHeader = httpServletRequest.getHeader(jwtUtil.getHeader());
        if (authHeader != null && !StringUtil.isEmpty(authHeader)) {
            String username = jwtUtil.getUsernameFromToken(authHeader);
            if (username != null 
                && SecurityContextHolder.getContext().getAuthentication() == null) {
                // 根据 username 查询用户,可以从缓存、数据库中获取
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                // 校验
                if (jwtUtil.validateToken(authHeader, userDetails)) {
                    // 构建 authentication
                    UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(userDetails,
                                                                null,
                                                                userDetails.getAuthorities());
                    // 设置 details,其中包含地址、session 等
                    authentication.setDetails(new 
                                              WebAuthenticationDetails(httpServletRequest));
                    // 设置 authentication 到上下文对象中
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

代码语言:javascript
复制
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    private final FilterInvocationSecurityMetadataSource superMetadataSource;
    private final Map<String, String[]> urlRoleMap = new HashMap<>();

    public MySecurityMetadataSource(
            FilterInvocationSecurityMetadataSource metadataSource) {
        this.superMetadataSource = metadataSource;
        // 此处可以从数据库加载权限配置
        urlRoleMap.put("/api/demo/admin", new String[]{"ROLE_admin"});
        urlRoleMap.put("/api/demo/user", new String[]{"ROLE_user", "ROLE_admin"});
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation) object;
        String url = fi.getRequestUrl();
        for (Map.Entry<String, String[]> entry : urlRoleMap.entrySet()) {
            if (antPathMatcher.match(entry.getKey(), url)) {
                // 生成 ConfigAttribute
                return SecurityConfig.createList(entry.getValue());
            }
        }
        // 返回配置类定义的默认权限配置
        return superMetadataSource.getAttributes(object);
    }
}

代码语言:javascript
复制
http.authorizeRequests()
    .anyRequest().authenticated()
    .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
        @Override
        public <O extends FilterSecurityInterceptor> O postProcess(O object) {
            // 设置为自定义的 SecurityMetadataSource
            object.setSecurityMetadataSource(mySecurityMetadataSource);
            // AffirmativeBased 是 AccessDecisionManager 的一种
            // AffirmativeBased,有一个投票器通过就通过
            // UnanimousBased,有一个投票器不通过就不通过,全部弃权也不通过
            object.setAccessDecisionManager(new AffirmativeBased(
                Arrays.asList(
                    new WebExpressionVoter(),
                    new RoleVoter()
                )));
            return object;
        }
    })
/**
 * 如果使用 UnanimousBased
 * 到达 RoleVoter 的 ConfigAttribute 是从数据库动态获取的,可能有多个
 * UnanimousBased 对每个 ConfigAttribute 进行投票,即所有权限都有才算通过
 */

po, dto,vo

post body请求参数,命名规范 XxRequest

展示层对象命名,XxVo

数据传输对象命名,XxDto

es实体名命名 XxIndexDO

db实体命名 跟表名相同

mongo实体命名 XxDoc

db组合关联实体命名 Xx

service接口命名 XxService

service实现命名 XxServiceImpl

manager,service引入多个manager进行负责的组合业务处理 XxManager

dao层命名 XxMapper

封装持久组合服务 XxRepository

代码语言:javascript
复制
apitest
bean
 dto
  CoolBoyDto
  CoolGirlDto
 po
  CoolBoy
  CoolGril
 vo
  CoolBoyVo
 GrilTypeEnums
cache
converter
 BoyGirlConverter
model
repository
request
service
22a417e9ccda4352b4b6ea5c6c33230d.png

image.png

logback.xml

代码语言:javascript
复制
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
代码语言:javascript
复制
server {

listen 80;

server_name xx.com;

charset utf-8;

 location / {

alias xxn-front/dist/;

try_files uri uri/ /index.html;

index  index.html index.htm;

}

location /api {

proxy_pass http://localhost:xxx/api;

proxy_set_header x-forwarded-for  $remote_addr;

}

}

07bdfbc137e4e958f5391580319fa68f.png

image.png

2a7db92a6128691f18fe885554b9a0ab.png

image.png

700a5a5614b26c1ba0a2a8f95e68d12d.png

image.png

代码语言:javascript
复制
##qq登陆相关##

qq.app.id=xxx

qq.app.key=xxx

qq.url.authorization=https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&state=%s

qq.url.access.token=https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s

qq.url.openid=https://graph.qq.com/oauth2.0/me?access_token=%S

qq.url.user.info=https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=%s&openid=%s

qq.url.redirect=http://easypan.wuhancoder.com/qqlogincalback

邮箱配置

1、邮箱配置

#发送邮件的邮箱,建议就试用qq邮箱

spring.mail.username=test@qq.com

#发送邮箱的密码

spring.mail.password=123

qq登录:

设置->账户->POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务

微信登录QQ邮箱:

个人头像->设置->第三方服务

安装Redis

下载Redis

下载地址:https://wwur.lanzout.com/iD8Ow0ti96dg 密码:4y2e

安装

解压直接双击安装,无需修改配置,一路下一步即可

a74ae953de31f126ec929bfb79fcbac7.png

安装ffmpeg

下载

下载地址:https://wwur.lanzout.com/iORvc0tia6uj 密码:9n15

查看版本

然后开启doc执行ffmpeg -verison

搜索gitlab镜像

由于Mac M1芯片区别去Intel,所以在找镜像的时候需要勾选ARM 64,然后一般推荐的镜像就是gitlab-ce。

8b3dabcecf3ca27204c2a7eafb9d28cb.png

image.png

7e305478366016ab7bc67ea0886adbad.png

image.png

代码语言:javascript
复制
docker run 
  -itd  
  --detach 
  --restart always 
  --name gitlab-ce 
  --privileged 
  --memory 4096M 
  --publish 9922:22 
  --publish 9980:80 
  --volume 在本地创建一个文件夹保存映射的文件/etc:/etc/gitlab:z 
  --volume 在本地创建一个文件夹保存映射的文件/log:/var/log/gitlab:z 
  --volume 在本地创建一个文件夹保存映射的文件/opt:/var/opt/gitlab:z 
  yrzr/gitlab-ce-arm64v8:latest
代码语言:javascript
复制
// 进入容器
docker exec -it gitlab-ce /bin/bash

// 修改gitlab.rb 如图1
vi /etc/gitlab/gitlab.rb

// 在最下面加入以下代码
// gitlab地址,端口默认为80端口
external_url 'http://192.168.124.194'

// ssh主机ip
gitlab_rails['gitlab_ssh_host'] = '192.168.124.194'

// ssh连接端口
gitlab_rails['gitlab_shell_ssh_port'] = 9922

// 修改http和ssh配置,如图2
vi /opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml

注意此处的host为线上服务器IP,或者改为域名,如果没有则不需要修改

// 修改成功后重启
gitlab-ctl restart

// 退出容器
exit

代码语言:javascript
复制
// 进入容器
docker exec -it gitlab /bin/bask

// 进入控制台
gitlab-rails console -e production

// 查询id为1的账号,1默认是超级管理员
User.where(id:1).first

// 修改密码 密码如果只有数字无法保存
user.password='abc123456'

// 保存修改 如果返回true则表示保存成功
user.save!

// 退出容器
exit

portainer是一款Docker可视化工具,可以方便我们查看和管理Container和Image

打开终端输入命令敲回车

代码语言:javascript
复制
docker run -d -v "/var/run/docker.sock:/var/run/docker.sock" -p 9000:9000 portainer/portainer

安装完成之后运行

代码语言:javascript
复制
docker run -d -p 9000:9000 --restart=always -v /var/run/docker.sock:/var/run/docker.sock --name portainer  docker.io/portainer/portainer

浏览器打开 localhost:9000

Lombok(不建议)

  • @Getter/@Setter
  • @ToString
  • @EqualsAndHashCode
  • @NoArgsConstructor
  • @AllArgsConstructor
  • @RequiredArgsConstructor
  • @Data
  • @Value
  • @Builder
  • @Slf4j

缺点依赖jdk,版本,插件

cbba7df93077fbbdb574ba65a8e33436.png

image.png

99c838783b2abdb9da33a87f25426695.png

image.png

代码语言:javascript
复制
<!--jsonwebtoken 生成token的库 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
代码语言:javascript
复制
TokenService
代码语言:javascript
复制
createToken
代码语言:javascript
复制
parseToken
c5bf1ccd8fc4bfc9361bc2164cc9711f.png

image.png

代码语言:javascript
复制
Claims claims = Jwts.parser() .setSigningKey("my-123") .parseClaimsJws(token) .getBody();

接口

代码语言:javascript
复制
@RequestMapping(value = '/login')
public Object vLogin(@RequestParam(value = "username") String username, @RequestParam(value = "password") String password) {
 Map<String, Object> map = new HashMap<>();
 // if (TextUtils.Isempty(username) || TextUtils.Isempty(password)) {
 // else
 User getUser = userService.validLogin(username, password);
 // 如果用户
 // getUser != null
 
 if(getUser!=null){
 String token=CreateJwt.getoken(getUser);
 map.put("user",getUser);
 map.put("token",token);
 map.put("msg", "登录成功");
}

刷新

代码语言:javascript
复制
@RequestMapping("/tokensign")
public Object tokenSign(@RequestParam(value = "token")String token){

Map<String,Object>map=new HashMap<>();
// 判断token是否为null
Claims claims = Jwts.parser().setSigningKey("my-123").parseClaimsJws(token).getBody();

Integer id=Integer.valueOf(claims.getId());
System.out.println("用户时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"). format(claims.getIssuedAt()));
System.out.println("过期时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"). format(claims.getExpiration()));
String username=claims.getSubject();
User user=userService.querybyid(id);

f(username!=null&&claims.getId()!=null&&username.equals(user.getUsername())){
String gettoken=CreateJwt.getoken(user);
map.put("user",user);
map.put("token",token);

return map;

访问权限进行控制

应用的安全性包括用户认证(Authentication)用户授权(Authorization)两个部分。

  • 用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码来完成认证过程。
  • 用户授权:验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。
代码语言:javascript
复制
<!-- spring security 安全认证 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

启动日志增加了如下内容,通过该内容可以找到默认用户名密码

security JSESSIONID 登录后的用户信息默认在 Cookies

自定义登录认证逻辑

@Slf4j @Component @RequiredArgsConstructor

c853babc4dfea3cba57af2f32da55855.png

image.png

不建议使用lombok

代码语言:javascript
复制
UserDetailsServiceImpl
代码语言:javascript
复制
用户验证处理
代码语言:javascript
复制
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

LoginFailureHandler
// 账号过期
log.info("[登录失败] - 用户账号过期");
log.info("[登录失败] - 用户密码错误");
log.info("[登录失败] - 用户密码过期");
log.info("[登录失败] - 用户被禁用");
log.info("[登录失败] - 用户被锁定");

代码语言:javascript
复制
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
代码语言:javascript
复制
SecurityConfig
代码语言:javascript
复制
WebSecurityConfig
/** 登录成功的处理 /
private final LoginSuccessHandler loginSuccessHandler;
/
* 登录失败的处理 */
private final LoginFailureHandler loginFailureHandler;

/** 配置认证方式等 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
 auth.userDetailsService(mingYueUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}

/** http相关的配置,包括登入登出、异常处理、会话管理等 */
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
http.authorizeRequests()
 // 放行接口
 // .antMatchers().permitAll()
 // 除上面外的所有请求全部需要鉴权认证
 .anyRequest()
 .authenticated()
 // 登入
 .and()
 .formLogin()
 // 允许所有用户
 .permitAll()
 // 登录成功处理逻辑
 .successHandler(loginSuccessHandler)
 // 登录失败处理逻辑
 .failureHandler(loginFailureHandler);
}

代码语言:javascript
复制
<!-- redis 缓存操作 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
代码语言:javascript
复制
lettuce:
  pool:
    # 连接池中的最小空闲连接
    min-idle: 0
    # 连接池中的最大空闲连接
    max-idle: 8
    # 连接池的最大数据库连接数
    max-active: 8
    # #连接池最大阻塞等待时间(使用负值表示没有限制)
    max-wait: -1ms
代码语言:javascript
复制
<!-- redis 缓存操作 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- pool 对象池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

代码语言:javascript
复制
RedisCache
代码语言:javascript
复制
/**
 * 缓存基本的对象,Integer、String、实体类等
 *
 * @param key 缓存的键值
 * @param value 缓存的值
 */
public <T> void setCacheObject(final String key, final T value)
{
    redisTemplate.opsForValue().set(key, value);
}
86be62b6dd8c490edf7b4b646f09ab05.png

image.png

89baa6aa7b9b07612be987d4bba7cee3.png

image.png

b3963573732d352805bee30a1038b9e8.png

image.png

0d5c4ac2383609f8f2168d4ff7c6d9c1.png

image.png

  • GenericToStringSerializer: 可以将任何对象泛化为字符串并序列化
  • Jackson2JsonRedisSerializer: 跟JacksonJsonRedisSerializer实际上是一样的
  • JacksonJsonRedisSerializer: 序列化object对象为json字符串
  • JdkSerializationRedisSerializer: 序列化java对象
  • StringRedisSerializer: 简单的字符串序列化

我们可以根据redis操作的不同数据类型,设置对应的序列化方式。

默认使用的是JdkSerializationRedisSerializer. 这种序列化最大的问题就是存入对象后,我们很难直观看到存储的内容,很不方便我们排查问题

而一般我们最经常使用的对象序列化方式是:Jackson2JsonRedisSerializer

代码语言:javascript
复制
RedisConfig
6c4c713ff5052ef3951e405bd934f7bf.png

image.png

可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行
这把锁要是一把可重入锁(避免死锁)
这把锁最好是一把阻塞锁
有高可用的获取锁和释放锁功能
获取锁和释放锁的性能要好

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

登录注册

代码语言:javascript
复制
import { validUsername, isNumber } from '@/utils/validate'
export default {
  name: 'Login',
    data() {
       const validatePhoneNo = (rule, value, callback) => {
          if (!(value.length === 11 && isNumber(value))) {
            callback(new Error('手机号码必须是11位数字'))
          } else {
            if (value.charAt(0) !== '1' || parseInt(value.charAt(1)) < 3) {
              callback(new Error('输入的手机号码不是有效的手机号'))
            } else {
              callback()
            }
          }
        }
        const validatePhoneCode = (rule, value, callback) => {
          if (!value) {
            callback(new Error('验证码不能为空'))
          } else {
            if (!(value.length === 6 && isNumber(value))) {
              callback(new Error('验证码必须是6位数字'))
            } else {
              callback()
            }
          }
        } 
    }

    return {
     loginRules: {
        username: [{ required: true, trigger: 'blur', validator: validateUsername }],
        password: [{ required: true, trigger: 'blur', validator: validatePassword }],
        phoneNo: [{ required: true, trigger: 'blur', validator: validatePhoneNo }],
        phoneCode: [{ required: true, trigger: 'blur', validator: validatePhoneCode }]
      }
      // 其他返回对象在此省略
    }
}

/**
 * 是否数字
 * @param {String} val
 * @returns {Boolean}
 */
export function isNumber(val) {
  for (let i = 0; i < val.length; i++) {
    if (val.charCodeAt(i) < 48 || val.charCodeAt(i) > 57) {
      return false
    }
  }
  return true
}
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" autocomplete="on" label-position="left">

代码语言:javascript
复制
handleLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.loading = true
          if (this.activeLoginType === '1') {
            const username = this.loginForm.username
            const password = this.loginForm.password
            this.$store.dispatch('user/login', { username: username, password: password })
              .then(() => {
                this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
                this.loading = false
              })
          } else {
            const phoneNo = this.loginForm.phoneNo
            const phoneCode = this.loginForm.phoneCode
            this.$store.dispatch('user/mobileLogin', { phoneNo: phoneNo, phoneCode: phoneCode })
              .then(() => {
                this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
                this.loading = false
              })
          }
        }
      })
    }
代码语言:javascript
复制
import { login, logout, phoneCodeLogin } from '@/api/user'
    
const actions = {
  // user login
  login({ commit }, userInfo) {
    const { username, password } = userInfo
    return new Promise((resolve, reject) => {
      login({ username: username, password: password }).then(response => {
        if (response.status === 200 && response.data) {
          const data = response.data.userInfo
          const useBaseInfo = {
            username: data.username,
            nickname: data.nickname,
            email: data.email,
            phoneNum: data.phoneNum
          }
          window.sessionStorage.setItem('userInfo', JSON.stringify(useBaseInfo))
          const { roles, currentRole } = data
          roles[0] = currentRole
          commit('SET_TOKEN', useBaseInfo)
          commit('SET_NAME', useBaseInfo.username)
          setToken(currentRole.id)
          commit('SET_ROLES', roles)
          window.sessionStorage.setItem('roles', JSON.stringify(roles))
          commit('SET_CURRENT_ROLE', currentRole)
          window.sessionStorage.setItem('currentRole', currentRole)
          // commit('SET_AVATAR', avtar)
          getRouteIds(currentRole.id).then(response => {
            if (response.status === 200 && response.data.status === 200) {
              const routeIds = response.data['data']
              window.sessionStorage.setItem('routeData', JSON.stringify(routeIds))
            } else {
              Message.error('response.status=' + response.status + 'response.text=' + response.text)
            }
          })
          resolve(useBaseInfo)
        } else {
          Message.error('user login failed')
          resolve()
        }
      }).catch(error => {
        console.error(error)
        reject(error)
      })
    })
  },
  // phone code login
  mobileLogin({ commit }, phoneParam) {
    const { phoneNo, phoneCode } = phoneParam
    return new Promise((resolve, reject) => {
      phoneCodeLogin({ phoneNo: phoneNo, phoneCode: phoneCode }).then(res => {
        if (res.status === 200 && res.data) {
          const data = res.data.userInfo
          const useBaseInfo = {
            username: data.username,
            nickname: data.nickname,
            phoneNum: data.phoneNum,
            email: data.email
          }
          window.sessionStorage.setItem('userInfo', JSON.stringify(useBaseInfo))
          const { roles, currentRole } = data
          roles[0] = currentRole
          commit('SET_TOKEN', useBaseInfo)
          commit('SET_NAME', useBaseInfo.username)
          setToken(currentRole.id)
          commit('SET_ROLES', roles)
          window.sessionStorage.setItem('roles', JSON.stringify(roles))
          commit('SET_CURRENT_ROLE', currentRole)
          window.sessionStorage.setItem('currentRole', currentRole)
          // commit('SET_AVATAR', avtar)
          getRouteIds(currentRole.id).then(response => {
            if (response.status === 200 && response.data.status === 200) {
              const routeIds = response.data['data']
              window.sessionStorage.setItem('routeData', JSON.stringify(routeIds))
            } else {
              Message.error('response.status=' + response.status + 'response.text=' + response.text)
            }
          })
          resolve(useBaseInfo)
        } else {
          Message.error('phone code login failed')
          resolve()
        }
      }).catch(error => {
        console.error(error)
        reject(error)
      })
    })
  },
    // 其他请求此处省略
}

可以在 jwt.io Debugger[1] 网站来解码、验证和生成 JWT。

由于缺乏安全性,不应该将敏感的会话数据存储在浏览器中。每当用户需要访问受保护的路由或资源时,用户代理应该发送jwt,通常在 Authorization header 中使用 Bearer 模式。

代码语言:javascript
复制
<!--加解密依赖-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.11</version>
        </dependency>
        <!--持久层框架mybatis-plus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.2</version>
        </dependency>
       <!--spring security依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.2.7.RELEASE</version>
        </dependency>
        <!--jwt token依赖-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.7.0</version>
        </dependency>
代码语言:javascript
复制
public class JwtTokenUtil {

    // 密钥
    private static final String SECRET = "bonusBACKEND2022$";

    // 过期时间7天
    private static final int EXPIRE_SECONDS = 7243600;

    private final static Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);

    /**
     * 生成token方法
     * @param memInfoMap
     * @return jwtToken
     */
    public static String genAuthenticatedToken(Map<String, Object> memInfoMap){
        List<GrantedAuthority> authorities = (List<GrantedAuthority>) memInfoMap.get("authorities");
        String authorityStr = null;
        if(authorities!=null && authorities.size()>0){
            StringBuffer buffer = new StringBuffer();
            for(int i=0; i<authorities.size()-1; i++){
                buffer.append(authorities.get(i).getAuthority()).append(",");
            }
            buffer.append(authorities.get(authorities.size()-1).getAuthority());
            authorityStr = buffer.toString();
        }
        String[] authorityArray = authorityStr!=null?authorityStr.split(","):null;
        Calendar nowTime = Calendar.getInstance();
        //过期时间
        nowTime.add(Calendar.SECOND, EXPIRE_SECONDS);
        Date expireDate = nowTime.getTime();
        String jwtToken = JWT.create().withJWTId(UUID.randomUUID().toString().replaceAll("-", ""))
                .withClaim("memId", (Long) memInfoMap.get("memId"))
                .withClaim("memAccount", (String) memInfoMap.get("memAccount"))
                .withClaim("memPwd", (String) memInfoMap.get("memPwd"))
                .withClaim("totalCreditAmount", ((BigDecimal) memInfoMap.get("totalCreditAmount")).doubleValue())
                .withClaim("usedCreditAmount", ((BigDecimal) memInfoMap.get("usedCreditAmount")).doubleValue())
                .withClaim("remainCreditAmount", ((BigDecimal) memInfoMap.get("remainCreditAmount")).doubleValue())
                .withArrayClaim("authorities", authorityArray)
                .withIssuedAt(new Date(System.currentTimeMillis()))
                .withExpiresAt(expireDate)
                .sign(Algorithm.HMAC256(SECRET));
        return jwtToken;
    }
}

JwtTokenUtil的工具类用于生成jwt令牌

代码语言:javascript
复制
public class JwtTokenUtil {

    // 密钥
    private static final String SECRET = "bonusBACKEND2022$";

    // 过期时间7天
    private static final int EXPIRE_SECONDS = 7243600;

    private final static Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);

    /**
     * 生成token方法
     * @param memInfoMap
     * @return jwtToken
     */
    public static String genAuthenticatedToken(Map<String, Object> memInfoMap){
        List<GrantedAuthority> authorities = (List<GrantedAuthority>) memInfoMap.get("authorities");
        String authorityStr = null;
        if(authorities!=null && authorities.size()>0){
            StringBuffer buffer = new StringBuffer();
            for(int i=0; i<authorities.size()-1; i++){
                buffer.append(authorities.get(i).getAuthority()).append(",");
            }
            buffer.append(authorities.get(authorities.size()-1).getAuthority());
            authorityStr = buffer.toString();
        }
        String[] authorityArray = authorityStr!=null?authorityStr.split(","):null;
        Calendar nowTime = Calendar.getInstance();
        //过期时间
        nowTime.add(Calendar.SECOND, EXPIRE_SECONDS);
        Date expireDate = nowTime.getTime();
        String jwtToken = JWT.create().withJWTId(UUID.randomUUID().toString().replaceAll("-", ""))
                .withClaim("memId", (Long) memInfoMap.get("memId"))
                .withClaim("memAccount", (String) memInfoMap.get("memAccount"))
                .withClaim("memPwd", (String) memInfoMap.get("memPwd"))
                .withClaim("totalCreditAmount", ((BigDecimal) memInfoMap.get("totalCreditAmount")).doubleValue())
                .withClaim("usedCreditAmount", ((BigDecimal) memInfoMap.get("usedCreditAmount")).doubleValue())
                .withClaim("remainCreditAmount", ((BigDecimal) memInfoMap.get("remainCreditAmount")).doubleValue())
                .withArrayClaim("authorities", authorityArray)
                .withIssuedAt(new Date(System.currentTimeMillis()))
                .withExpiresAt(expireDate)
                .sign(Algorithm.HMAC256(SECRET));
        return jwtToken;
    }
}

实现用户认证方法
代码语言:javascript
复制
@Service
public class MemInfoServiceImpl extends ServiceImpl<MemInfoMapper, MemInfoDTO> implements MemInfoService {
 private final static Logger logger = LoggerFactory.getLogger(MemInfoServiceImpl.class);
    @Resource
    private MyPasswordEncoder passwordEncoder;
    @Resource
    private RoleInfoService roleInfoService;
    
    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MemInfoDTO memInfoDTO = this.baseMapper.getMemInfoByAccount(username);
        if(memInfoDTO==null){
            throw  new UsernameNotFoundException("Username" + username + "is invalid!");
        }
        // 获取用户角色列表
        List<RoleInfoDTO> roleInfoDTOList = roleInfoService.getRolesByMemId(memInfoDTO.getMemId());
        if(roleInfoDTOList.size()>0){
            for(RoleInfoDTO roleInfoDTO: roleInfoDTOList){
                SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + roleInfoDTO.getRoleName().toUpperCase());
                memInfoDTO.getAuthorities().add(grantedAuthority);
            }
        }
        return memInfoDTO;
    }
代码语言:javascript
复制
@Data
@TableName("bonus_mem_info")
@ApiModel(value="MemInfoDTO", description = "会员DTO")
@Validated
public class MemInfoDTO extends BaseDTO implements UserDetails {

    /**
     * 会员id
     */
    @TableId
    @ApiModelProperty(name = "memId", value = "memId", notes = "会员ID", dataType = "Long")
    private Long memId;

    /**
     * 会员账号
     */
    @TableField(value = "mem_account")
    @NotEmpty(message = "会员账号不能为空")
    @ApiModelProperty(name="memAccount", value = "memAccount", notes = "会员账号", dataType = "String")
    private String memAccount;

    /**
     * 会员密码
     */
    @TableField(value = "mem_pwd")
    @NotEmpty(message = "会员密码不能为空")
    @ApiModelProperty(name="memPwd", value = "memPwd", notes = "加密后的会员密码", dataType = "String")
    private String memPwd;

    /**
     * 会员类型:1-vip;2-代理
     */
    @TableField(value = "mem_type")
    @NotEmpty(message = "会员类型不能为空")
    @ApiModelProperty(name="memType", value = "memType", notes = "会员类型", dataType = "Integer", example = "1", allowableValues = "1,2")
    private Integer memType;

    /**
     * 会员信用额度,单位分
     */
    @TableField(value = "total_credit_amount")
    @NotEmpty(message = "会员信用额度不能为空")
    @ApiModelProperty(name = "totalCreditAmount", value = "totalCreditAmount", notes = "会员总信用额度,单位分", dataType = "Long", example = "10000")
    private Long totalCreditAmount;

    /**
     * 会员已使用信用额度,单位分
     */
    @ApiModelProperty(name = "usedCreditAmount", value = "usedCreditAmount", notes = "会员已使用信用额度,单位分", dataType = "Long", example = "5000")
    @TableField(value = "used_credit_amount")
    private Long usedCreditAmount;

    @TableField(exist = false)
    private List<GrantedAuthority> authorities = new ArrayList<>();

    @Override
    public Collection<GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return this.memPwd;
    }

    @Override
    public String getUsername() {
        return this.memAccount;
    }

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

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

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

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

代码语言:javascript
复制
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final static Logger logger = LoggerFactory.getLogger(SecurityConfig.class);

    @Resource
    private MemInfoService memInfoService;

    private MathContext mathContext = new MathContext(2, RoundingMode.HALF_UP);

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
        auth.userDetailsService(memInfoService);
    }

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/static/","/index.html","/templates/", "/admin/", "/doc.html", "/webjars/", "/v2/*", "/favicon.ico", "/swagger-resources");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        JwtAuthenticationFilterBean jwtAuthenticationFilterBean = new JwtAuthenticationFilterBean();
        http.addFilterBefore(jwtAuthenticationFilterBean, UsernamePasswordAuthenticationFilter.class); // 将JwtToken认证过滤器注册在登录认证过滤器之前
        // 配置跨域
        http.cors().configurationSource(corsConfigurationSource())
                .and().logout().invalidateHttpSession(true).logoutUrl("/member/logout").permitAll()
        ;
        http.authorizeRequests().antMatchers("/member/checkSafetyCode").permitAll()
                .antMatchers("/doc.html").permitAll()
                .antMatchers("/common/kaptcha").permitAll()
                .antMatchers("/admin/login").permitAll()
                .anyRequest().authenticated()
                .and().httpBasic()
                .and().formLogin()
                .loginProcessingUrl("/member/login") // 登录接口
                .successHandler((httpServletRequest, httpServletResponse, authentication) -> {
                     httpServletResponse.setContentType("application/json;charset=utf-8");
                     httpServletResponse.setStatus(HttpStatus.OK.value());
                     PrintWriter printWriter = httpServletResponse.getWriter();
                     MemInfoDTO memInfoDTO = (MemInfoDTO) authentication.getPrincipal();
                     Map<String, Object> userMap = new HashMap<>();
                     userMap.put("memId", memInfoDTO.getMemId());
                     userMap.put("memAccount", memInfoDTO.getMemAccount());
                     userMap.put("memPwd", memInfoDTO.getMemPwd());
                     BigDecimal totalCredit = memInfoDTO.getTotalCreditAmount()!=null?new BigDecimal(memInfoDTO.getTotalCreditAmount()/100, mathContext): new BigDecimal("0.0");
                     userMap.put("totalCreditAmount", totalCredit);
                     BigDecimal usedCredit = memInfoDTO.getUsedCreditAmount()!=null?new BigDecimal(memInfoDTO.getUsedCreditAmount()/100, mathContext):new BigDecimal("0.0");
                     userMap.put("usedCreditAmount", usedCredit);
                     Long remainCredit = (memInfoDTO.getTotalCreditAmount()==null?0:memInfoDTO.getTotalCreditAmount()) - (memInfoDTO.getUsedCreditAmount()==null?0:memInfoDTO.getUsedCreditAmount());
                     BigDecimal remainCreditAmount = new BigDecimal(remainCredit/100, mathContext);
                     userMap.put("remainCreditAmount", remainCreditAmount);
                     userMap.put("authorities", memInfoDTO.getAuthorities());
                     Map<String, Object> dataMap = new HashMap<>();
                     dataMap.put("memInfo", userMap);
                     dataMap.put("authenticatedToken", "Bearer "+JwtTokenUtil.genAuthenticatedToken(userMap));
                     ResponseResult<Map<String, Object>> responseResult = ResponseResult.success(dataMap, "login success");
                     printWriter.write(JSONObject.toJSONString(responseResult));
                     printWriter.flush();
                     printWriter.close();
                }).failureHandler((httpServletRequest, httpServletResponse, e) -> {
                     logger.error("login failed, caused by " + e.getMessage());
                     httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
                     httpServletResponse.setStatus(HttpStatus.OK.value());
                     PrintWriter printWriter = httpServletResponse.getWriter();
                     ResponseResult<String> responseResult = ResponseResult.error(HttpStatus.UNAUTHORIZED.value(), "authentication failed");
                     responseResult.setPath(httpServletRequest.getRequestURI());
                     printWriter.write(JSONObject.toJSONString(responseResult));
                     printWriter.flush();
                     printWriter.close();
                }).permitAll()
                .and().csrf().disable().exceptionHandling().accessDeniedHandler(accessDeniedHandler());

    }

    //配置跨域访问资源
    private CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source =   new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin(""); //同源配置,表示任何请求都视为同源,若需指定ip和端口可以改为如“localhost:8080”,多个以“,”分隔;
        corsConfiguration.addAllowedHeader("
");//header,允许哪些header,本案中使用的是token,此处可将
替换为token;
        corsConfiguration.addAllowedMethod("*"); //允许的请求方法,PSOT、GET等
        corsConfiguration.setAllowCredentials(true);
        // 注册跨域配置
        source.registerCorsConfiguration("/**",corsConfiguration); //配置允许跨域访问的url
        return source;
    }

    @Bean
    AccessDeniedHandler accessDeniedHandler() {
        return new AuthenticationAccessDeniedHandler();
    }
}

e0dc156aa1b091fc0e63e2d055ec8260.png

image.png

仓库地址:https://github.com/webVueBlog/JavaGuideInterview