SpringClond微服务架构篇 五 、整合security鉴权篇

 Spring Security介绍


Spring Security是一个Java框架,用于保护应用程序的安全性。它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。Spring Security基于过滤器链的概念,可以轻松地集成到任何基于Spring的应用程序中。它支持多种身份验证选项和授权策略,开发人员可以根据需要选择适合的方式。此外,Spring Security还提供了一些附加功能,如集成第三方身份验证提供商和单点登录,以及会话管理和密码编码等。总之,Spring Security是一个强大且易于使用的框架,可以帮助开发人员提高应用程序的安全性和可靠性。

使用版本:

spring-boot-starter-security 2.7.10

springboot全家桶依赖 2.7.10

使用写法:

security 共两种写法 分别为mvc 与webflux 

本系统时间有限采用mvc方式,经典且自由。

思路一:如果采用webflux  可以与网关gateway模块合并鉴权。

一、加入依赖

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

二、配置文件配置(只给出了权限部分的)

jwt:
  token:
    header: Authorization
    expired: 10000 #token 过期时间 单位秒
    secret: cereshuzhitingnizhenbangcereshuzhitingnizhenbang #token 密钥
    refresh:
      expired: 30  #token 刷新时间 单位秒
security:
  noFilter:
    - /swagger-ui/**    # Swagger UI v3
    - /swagger-resources/**
    - /v2/api-docs/**  # Swagger v2
    - /auth/v2/api-docs
    - /v2/api-docs  # Swagger v2
    - /**/v2/api-docs  # Swagger v2
    - /webjars/**       # Webjars, typically for static resources
    - /favicon.ico      # Favicon
    - /css/**           # CSS files
    - /js/**            # JavaScript files
    - /images/**        # Images
    - /public/**        # Public folder
    - /static/**         # Static resources
    - /doc.html
    - /webjars/**
    - /system/logout
    - /login
    - /permission/**
    - /getCaptcha  #获取验证码
    - /system/sysUser/list
    - /**/doc.html

三、配置读取

SecurityPathConfig  白名单配置读取
JwtConfig  jwt token配置读取

JwtConfig.java

import lombok.Data;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@ConfigurationProperties("jwt.token")
@Configuration
public class JwtConfig {
    //token 密钥
    private String secret;
    //token 过期时间 单位秒
    private Long expired;
    //token 刷新时间 单位秒
    @Value("${jwt.token.refresh.expired}")
    private Long refreshExpired;
    //header 请求鉴权头
    private String header;


}

 SecurityPathConfig.java

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@ConfigurationProperties("security")
@Configuration
@Data
public class SecurityPathConfig {

    List<String> noFilter;
}

四、封装User对象实体

LoginDto为获取用户实体 根据自己的用户实体进行放入

import cn.hutool.core.lang.Assert;
import com.pinyi.supply.system.dto.LoginDto;
import com.pinyi.supply.system.model.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

public class LoginUser implements UserDetails {

    private LoginDto loginDto;

    public LoginDto getLoginDto() {
        return loginDto;
    }

    public void setLoginDto(LoginDto loginDto) {
        this.loginDto = loginDto;
    }

    private static final long serialVersionUID = 540L;
    private String password;

    private final String username;
    private final Collection<? extends GrantedAuthority> authorities;
    private final boolean accountNonExpired;
    private final boolean accountNonLocked;
    private final boolean credentialsNonExpired;
    private final boolean enabled;


    public LoginUser(LoginDto loginDto, String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this(loginDto, username, password, true, true, true, true, authorities);
    }

    public LoginUser(LoginDto loginDto, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        Assert.isTrue(username != null && !"".equals(username) && password != null, "Cannot pass null or empty values to constructor");
        this.loginDto = loginDto;

        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.accountNonExpired = accountNonExpired;
        this.credentialsNonExpired = credentialsNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.authorities = authorities;
    }

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

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


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

    @Override
    public boolean isAccountNonExpired() {
        return this.accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return this.accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return this.credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }
}

五、自定义获取用户

import com.pinyi.security.security.entity.LoginUser;
import com.pinyi.supply.system.dto.LoginDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    SysUserService sysUserService;


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


        System.out.println("查询用户");

        LoginDto userDto = sysUserService.getByUsername(username);
        if (userDto == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return new LoginUser(userDto, userDto.getSysUser().getLoginName()
                , userDto.getSysUser().getPassword(), getUserAuthority(userDto.getPermissions()));
    }

    /**
     * 获取用户权限信息(角色、菜单权限)
     *
     * @param permissions
     * @return
     */
    public List<GrantedAuthority> getUserAuthority(List<String> permissions) {
        // 实际怎么写以数据表结构为准,这里只是写个例子
        // 角色(比如ROLE_admin),菜单操作权限(比如sys:user:list)
        List<GrantedAuthority> listPram = new ArrayList<>();
        for (String s : permissions) {
            listPram.add(new SimpleGrantedAuthority(s));
        }
        return listPram;
    }
}

六、查询用户service

remoteSystemService 采用的Feign 调用的用户服务获取信息。
@Service
public class SysUserService {


    @Resource
    private RemoteSystemService remoteSystemService;

    public LoginDto getByUsername(String userName) {

        LoginDto result = remoteSystemService.getUserByUsername(userName);

        if (ObjectUtils.isEmpty(result)) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return result;
    }
}

Feign 接口实例

import com.pinyi.security.security.feign.factory.RemoteAuthServiceFactory;
import com.pinyi.supply.common.http.HttpResult;
import com.pinyi.supply.system.dto.LoginDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(contextId = "systemService", value = "system", fallbackFactory = RemoteAuthServiceFactory.class)
public interface RemoteSystemService {


    @GetMapping("/sysUser/getUserByUsername")
    public LoginDto getUserByUsername(@RequestParam(value = "username") String username);
}

    七、登录实现

@Api(tags = "用户授权接口")
@RestController
@RequestMapping
public class AuthController {

    @Autowired
    LoginService loginService;
    @Autowired
    JwtTokenUtils jwtUtils;


    /**
     * 用户登录接口
     * 该方法处理用户登录请求,通过POST方法提交登录信息
     *
     * @param username 用户名,用于识别用户身份
     * @param password 密码,用户账户的安全凭证
     * @param captcha  验证码,用于防止暴力破解,提高登录安全性
     */
    @ApiOperation(value = "登录接口", notes = "登录接口")
    @ApiResponses(value = {
            @ApiResponse(code = 200, message = "成功", response = HttpResult.class),
            @ApiResponse(code = 500, message = "服务器内部错误")
    })
    @PostMapping("/login")
    public void login(@ApiParam(value = "账号", required = true) @RequestParam(value = "username") String username,
                      @ApiParam(value = "密码", required = true) @RequestParam(value = "password") String password,
                      @ApiParam(value = "验证码", required = true) @RequestParam(value = "captcha") String captcha) {
        loginService.login(new LoginEntity(username, password, captcha));
    }





    /**
     * 获取验证码接口
     *
     * @param request  HTTP请求对象
     * @param response HTTP响应对象
     * @param username 用户名,用于识别请求验证码的用户
     * @return 返回HttpResult对象,包含验证码信息
     * <p>
     * 说明:
     * 1. 该方法通过调用loginService.addCaptcha(username)生成验证码,并将验证码与用户名关联后存储。
     * 2. 成功生成验证码后,通过HttpResult对象返回验证码信息。
     * 3. 该方法使用@GetMapping注解,指定请求URL为"/getCaptcha",处理GET类型的请求。
     */
    @ApiOperation(value = "获取验证码", notes = "获取验证码")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "username", value = "用户名", required = true, dataType = "String", paramType = "query")
    })
    @GetMapping("/getCaptcha")
    public HttpResult getCaptcha(HttpServletRequest request, HttpServletResponse response, String username) {
        String captcha = loginService.addCaptcha(username);
        return HttpResult.success("获取成功", captcha);
    }

}

登录service接口

import com.pinyi.security.security.entity.dto.LoginEntity;
import com.pinyi.supply.system.dto.LoginDto;

public interface LoginService {

    /**
     * 登录
     * @param entity 登录对象
     * @return token
     */
    String login(LoginEntity entity);


    /**
     * 获取用户信息
     * @return 用户信息
     */
    LoginDto getUserInfo();


    /**
     * 退出登录
     * @return 是否成功
     */
    Boolean logout();

    /**
     * 获取验证码
     * @param username 用户名
     * @return 验证码
     */
    String addCaptcha(String username);


}

实现service


import com.pinyi.security.security.entity.dto.LoginEntity;
import com.pinyi.supply.common.utils.StringUtils;
import com.pinyi.supply.redis.service.RedisService;
import com.pinyi.supply.system.dto.LoginDto;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;

import static com.pinyi.supply.redis.enuns.RedisCacheEnum.CAPTCHA_PATH;

@Service
public class LoginServiceImpl implements LoginService {


    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private RedisService redisService;


    /**
     * 登录方法
     *
     * @param entity 登录对象
     * @return 登录结果
     */
    @Override
    public String login(LoginEntity entity) {
        //进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(entity.getUsername(), entity.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //认证未通过,给出提示
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("登陆失败!");

        }
        return "";
    }

    /**
     * 获取用户信息
     *
     * @return 用户信息
     */
    @Override
    public LoginDto getUserInfo() {
        UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
         //details里面可能存放了当前登录用户的详细信息,也可以通过cast后拿到
        LoginDto loginDto = (LoginDto) authenticationToken.getPrincipal();
        if (loginDto != null && loginDto.getSysUser() != null) {
            loginDto.getSysUser().setPassword(null);
        }
        return loginDto;
    }

    @Override
    public Boolean logout() {
        return true;
    }

    /**
     * 生成并返回一个随机的验证码
     *
     * @param username 用户名,此参数在本方法中未被使用
     * @return 返回一个随机生成的验证码字符串
     * <p>
     * 方法说明:
     * 1. 该方法的作用是添加并返回一个随机验证码,验证码与特定用户无关,因此不使用username参数
     * 2. 验证码生成的具体逻辑在random3()方法中,此处应调用random3()以获取验证码
     * 3. 由于方法功能简单明了,故无需额外导入类或复杂逻辑
     */
    @Override
    public String addCaptcha(String username) {
        if (StringUtils.isBlank(username)) {
            throw new RuntimeException("用户名不能为空");
        }
        String code = random3();
        //redis存储
        redisService.set(CAPTCHA_PATH.getPath() + username, code, 120);
        return code;
    }

    /**
     * 生成并返回一个随机的验证码
     *
     * @return 返回一个随机生成的验证码字符串
     * <p>
     * 方法说明:
     * 1. 该方法的作用是添加并返回一个随机验证码,验证码与特定用户无关,因此不使用username参数
     * 2. 验证码生成的具体逻辑在random3()方法中,此处应调用random3()以获取验证码
     * 3. 由于方法功能简单明了,故无需额外导入类或复杂逻辑
     */
    private static String random3() {
        // jdk1.7出的随机生成数,关键还并发安全
        // nextInt(int origin, int bound) 范围:[origin,bound)
        return String.valueOf(ThreadLocalRandom.current().nextInt(100000, 1000000));
    }

}

八、security配置config

由于配置了加密

BCryptPasswordEncoder 所以用户密码要存储该方式加密字符
import com.pinyi.security.security.filter.CaptchaFilter;
import com.pinyi.security.security.filter.JwtAuthenticationEntryPoint;
import com.pinyi.security.security.filter.JwtAuthenticationFilter;
import com.pinyi.security.security.handler.*;
import com.pinyi.security.security.service.UserDetailServiceImpl;
import com.pinyi.security.security.utils.JwtTokenUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    LoginFailureHandler loginFailureHandler;

    @Autowired
    LoginSuccessHandler loginSuccessHandler;

    @Autowired
    CaptchaFilter captchaFilter;

    @Autowired
    JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    JwtAccessDeniedHandler jwtAccessDeniedHandler;

    @Autowired
    UserDetailServiceImpl userDetailService;

    @Autowired
    JWTLogoutSuccessHandler jwtLogoutSuccessHandler;
    @Autowired
    JwtCustomLogoutHandler jwtCustomLogoutHandler;


    @Autowired
    SecurityPathConfig securityPathConfig;
    @Autowired
    JwtTokenUtils jwtTokenUtils;

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

    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Bean
    JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
        return jwtAuthenticationFilter;
    }


    public String[] getOpenPaths() {
        return securityPathConfig.getNoFilter().toArray(new String[0]);
    }


//    @Bean
//    PasswordEncoder PasswordEncoder() {
//        return new PasswordEncoder();
//    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                // 登录配置
                .formLogin()
                .loginProcessingUrl("/login")
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler)

                .and()
                .logout()
                .addLogoutHandler(jwtCustomLogoutHandler)
                .permitAll()
                .logoutSuccessUrl("/")

                .logoutSuccessHandler(jwtLogoutSuccessHandler)

                // 禁用session
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // 配置拦截规则
                .and()
                .authorizeRequests()
                .antMatchers(getOpenPaths()).permitAll()
                .anyRequest().authenticated()
                // 异常处理器
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)

                // 配置自定义的过滤器
                .and()
                .addFilter(jwtAuthenticationFilter())
                // 验证码过滤器放在UsernamePassword过滤器之前
                .addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(jwtAuthenticationFilter(), LogoutFilter.class)
        ;
    }

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

九、拦截器处理

使用拦截器介绍

CaptchaFilter 验证码拦截器 (只登录时拦截)

import com.pinyi.security.security.exception.CaptchaException;
import com.pinyi.security.security.handler.LoginFailureHandler;
import com.pinyi.supply.redis.enuns.RedisCacheEnum;
import com.pinyi.supply.redis.service.RedisService;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;

@Component
@Log4j2
public class CaptchaFilter extends OncePerRequestFilter {

    private static final String LOGIN_URL = "/login";
    private static final String POST_METHOD = "POST";

    @Autowired
    LoginFailureHandler loginFailureHandler;
    @Autowired
    private RedisService redisService;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        log.debug("验证码拦截器处理");
        String url = httpServletRequest.getRequestURI();
        String method = httpServletRequest.getMethod();

        if (Objects.equals(LOGIN_URL, url) && Objects.equals(POST_METHOD, method)) {
            handleLoginValidation(httpServletRequest, httpServletResponse);
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        } else {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        }
    }


    // 处理登录验证码
    private void handleLoginValidation(HttpServletRequest request, HttpServletResponse response) throws CaptchaException, ServletException, IOException {
        try {
            validate(request);
        } catch (CaptchaException e) {
            log.error("验证码校验失败: {}", e.getMessage(), e);
            loginFailureHandler.onAuthenticationFailure(request, response, e);
        }
    }


    // 校验验证码逻辑
    private void validate(HttpServletRequest request) {
        String code = request.getParameter("captcha");
        String username = request.getParameter("username");

        if (StringUtils.isBlank(username) || StringUtils.isBlank(code)) {
            throw new CaptchaException("用户名与验证码不能为空");
        }

        if (redisService.hasKey(RedisCacheEnum.CAPTCHA_PATH.getPath() + username)) {
            String captcha = (String) redisService.get(RedisCacheEnum.CAPTCHA_PATH.getPath() + username);
            if (!captcha.equals(code)) {
                throw new CaptchaException("验证码错误");
            }
        } else {
            throw new CaptchaException("验证码已过期");
        }

        if (StringUtils.isBlank(code)) {
            throw new CaptchaException("验证码错误");
        }
    }
}

JwtAuthenticationEntryPoint 认证失败过滤器

import com.alibaba.fastjson.JSON;
import com.pinyi.security.security.enums.AuthorizationCodeEnum;
import com.pinyi.supply.common.http.HttpResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

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

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        logger.info("JWT认证失败处理器JwtAuthenticationEntryPoint");
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.setStatus(HttpServletResponse.SC_OK); // 使用常量代替数字提高可读性

        try (ServletOutputStream outputStream = httpServletResponse.getOutputStream()) {
            String jsonResult = JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TEMPORARILY_WITHOUT_AUTHORITY)); // 使用Jackson转换对象为JSON字符串
            outputStream.write(jsonResult.getBytes(StandardCharsets.UTF_8));
            outputStream.flush();
        } catch (IOException ex) {
            logger.error("Failed to write response", ex);
        }
    }
}

JwtAuthenticationFilter 每次请求(jwt身份)认证过滤器


import com.alibaba.fastjson.JSON;
import com.pinyi.security.security.config.JwtConfig;
import com.pinyi.security.security.config.SecurityPathConfig;
import com.pinyi.security.security.enums.AuthorizationCodeEnum;
import com.pinyi.security.security.service.SysUserService;
import com.pinyi.security.security.service.UserDetailServiceImpl;
import com.pinyi.security.security.utils.JwtTokenUtils;
import com.pinyi.security.security.utils.WhitelistChecker;
import com.pinyi.supply.common.exception.BusinessRuntimeException;
import com.pinyi.supply.common.http.HttpResult;
import com.pinyi.supply.common.utils.StringUtils;
import com.pinyi.supply.redis.enuns.RedisCacheEnum;
import com.pinyi.supply.redis.service.RedisService;
import com.pinyi.supply.system.dto.LoginDto;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    @Autowired
    JwtTokenUtils jwtUtils;

    @Autowired
    UserDetailServiceImpl userDetailService;

    @Autowired
    SysUserService sysUserService;
    @Autowired
    JwtConfig jwtConfig;
    @Autowired
    SecurityPathConfig securityPathConfig;
    @Autowired
    RedisService redisService;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String url = request.getRequestURI();
        System.out.println("JWT过滤器,url-" + url);
        response.setCharacterEncoding("UTF-8");

        String jwt = request.getHeader(jwtConfig.getHeader());
        // 这里如果没有jwt,继续往后走,因为后面还有鉴权管理器等去判断是否拥有身份凭证,所以是可以放行的
        // 没有jwt相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口
        //跳过不需要验证token
        WhitelistChecker checker = new WhitelistChecker(securityPathConfig.getNoFilter());
        if (checker.isWhitelisted(url)) {
            chain.doFilter(request, response);
            return;
        }


        //校验jwt
        if (StringUtils.isBlank(jwt)) {
            //token为空
            response.getWriter().write(JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TOKEN_NULL)));
            return;
        }

        try {
            UsernamePasswordAuthenticationToken authentication = getAuthentication(request, response);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            chain.doFilter(request, response);
        } catch (ExpiredJwtException e) {
            response.getWriter().write(JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TOKEN_EXPIRED)));
            logger.error("Token已过期: {} " + e);
        } catch (UnsupportedJwtException e) {
            response.getWriter().write(JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TOKEN_FORMAT_ERROR)));
            logger.error("Token格式错误: {} " + e);
        } catch (MalformedJwtException e) {
            response.getWriter().write(JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TOKEN_IS_NOT_CONSTRUCTED_CORRECTLY)));
            logger.error("Token没有被正确构造: {} " + e);
        } catch (IllegalArgumentException e) {
            response.getWriter().write(JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TOKEN_INVALID_PARAMETER_IS_ABNORMAL)));
            logger.error("非法参数异常: {} " + e);
        } catch (Exception e) {
            response.getWriter().write(JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TOKEN_LESS)));
            logger.error("无效token" + e.getMessage());
        }

    }

    //获取用户凭证
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request, HttpServletResponse response) {
        String token = request.getHeader(jwtConfig.getHeader());
        if (token != null) {
            String userName = "";
            LoginDto loginDto = null;
            try {
                // 解密Token
                userName = jwtUtils.getUserName(token);
                if (StringUtils.isNotBlank(userName)) {
                    // 判断redis是否包含该用户信息
                    boolean hasKey = redisService.hasKey(RedisCacheEnum.LOGIN_PATH.getPath() + userName + RedisCacheEnum.USER_PATH.getPath());
                    if (!hasKey) {
                    throw  new BusinessRuntimeException("登录已过期,请重新登录");
                    }
                    loginDto = JSON.parseObject(redisService.get(RedisCacheEnum.LOGIN_PATH.getPath() + userName + RedisCacheEnum.USER_PATH.getPath()).toString(), LoginDto.class);
                    // 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的JWT,实现自动登录
                    UsernamePasswordAuthenticationToken tokenAuth = new UsernamePasswordAuthenticationToken(loginDto, null, userDetailService.getUserAuthority(loginDto.getPermissions()));
                    return tokenAuth;

                }
            } catch (ExpiredJwtException e) {
                throw e;
                //throw new TokenException("Token已过期");
            } catch (UnsupportedJwtException e) {
                throw e;
                //throw new TokenException("Token格式错误");
            } catch (MalformedJwtException e) {
                throw e;
                //throw new TokenException("Token没有被正确构造");
            } catch (IllegalArgumentException e) {
                throw e;
                //throw new TokenException("非法参数异常");
            } catch (Exception e) {
                throw e;
                //throw new IllegalStateException("Invalid Token. "+e.getMessage());
            }
            return null;
        }
        return null;
    }

}

JwtAccessDeniedHandler 无权限访问

import com.alibaba.fastjson.JSON;
import com.pinyi.security.security.enums.AuthorizationCodeEnum;
import com.pinyi.supply.common.http.HttpResult;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * @author :Shixing
 * @date :Created in 2020/7/14 11:04
 * @description:无权限访问拦截
 */
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        System.out.println("无权限访问");
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
        ServletOutputStream outputStream = httpServletResponse.getOutputStream();
        outputStream.write(JSON.toJSONString(HttpResult.error(AuthorizationCodeEnum.TEMPORARILY_WITHOUT_AUTHORITY)).getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

JwtCustomLogoutHandler 自定义登出实现

import com.pinyi.supply.redis.enuns.RedisCacheEnum;
import com.pinyi.supply.redis.service.RedisService;
import com.pinyi.supply.system.dto.LoginDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class JwtCustomLogoutHandler implements LogoutHandler {

    @Autowired
    RedisService redisService;

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        System.out.println("自定义登出方法拦截");
        LoginDto loginDto = (LoginDto) authentication.getPrincipal();
        //删除redis缓存
        String[] keys = {
                RedisCacheEnum.LOGIN_PATH.getPath() + loginDto.getSysUser().getLoginName() + RedisCacheEnum.USER_PATH.getPath(),
                RedisCacheEnum.LOGIN_PATH.getPath() + loginDto.getSysUser().getLoginName() + RedisCacheEnum.PARAMETER_PATH.getPath(),
                RedisCacheEnum.LOGIN_PATH.getPath() + loginDto.getSysUser().getLoginName() + RedisCacheEnum.TOKEN_PATH.getPath()
        };
        redisService.del(keys);
        //TODO 记录登出日志

    }
}

JWTLogoutSuccessHandler 登出成功拦截


@Component
public class JWTLogoutSuccessHandler implements LogoutSuccessHandler {

    @Autowired
    JwtTokenUtils jwtUtils;

    @Autowired
    JwtConfig jwtConfig;


    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        System.out.println("登出方法拦截");
        if (authentication != null) {
            new SecurityContextLogoutHandler().logout(httpServletRequest, httpServletResponse, authentication);
        }

        httpServletResponse.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = httpServletResponse.getOutputStream();
        httpServletResponse.setHeader(jwtConfig.getHeader(), "");

        outputStream.write(JSONUtil.toJsonStr(HttpResult.success("登出成功")).getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

LoginFailureHandler 登录失败拦截


import cn.hutool.json.JSONUtil;
import com.pinyi.security.security.enums.AuthorizationCodeEnum;
import com.pinyi.security.security.exception.CaptchaException;
import com.pinyi.supply.common.http.HttpResult;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * 登录失败处理器
 *
 * @author shixing
 * @create 2021-05-07-22:06
 */
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {


    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        String lang = request.getHeader("lang");
        String msg = AuthorizationCodeEnum.UNKNOWN_ERROR.getMsg();
        if (exception instanceof BadCredentialsException) {
            msg = exception.getMessage();
        } else if (exception instanceof CaptchaException) {
            msg = exception.getMessage();
        } else if (exception != null) {
            msg = exception.getMessage();
            // 可以添加更多的异常处理逻辑
        }
        //返回错误信息
        try (ServletOutputStream outputStream = response.getOutputStream()) {
            outputStream.write(JSONUtil.toJsonStr(HttpResult.error(msg)).getBytes(StandardCharsets.UTF_8));
            outputStream.flush();
        }
        // 抛出异常,终止后续过滤器链的执行
        throw new ServletException("Authentication failed", exception);
    }
}

LoginSuccessHandler 登录成功拦截

import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.pinyi.security.security.config.JwtConfig;
import com.pinyi.security.security.entity.LoginUser;
import com.pinyi.security.security.utils.JwtTokenUtils;
import com.pinyi.supply.common.http.HttpResult;
import com.pinyi.supply.redis.enuns.RedisCacheEnum;
import com.pinyi.supply.redis.service.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    JwtTokenUtils jwtTokenUtils;
    @Autowired
    JwtConfig jwtConfig;
    @Autowired
    RedisService redisService;


    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        System.out.println("登录成功");
        httpServletResponse.setContentType("application/json;charset=UTF-8");
        ServletOutputStream outputStream = httpServletResponse.getOutputStream();
        //通过了,生成jwt
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        String token = jwtTokenUtils.generateToken(loginUser.getUsername());
        //将用户信息存入redis
        httpServletResponse.setHeader(jwtConfig.getHeader(), token);
        redisService.set(RedisCacheEnum.LOGIN_PATH.getPath() + loginUser.getUsername() + RedisCacheEnum.USER_PATH.getPath(),
                JSON.toJSONString(loginUser.getLoginDto()), jwtConfig.getExpired() + 600);
        redisService.set(RedisCacheEnum.LOGIN_PATH.getPath() + loginUser.getUsername() + RedisCacheEnum.PARAMETER_PATH.getPath()
                , JSON.toJSONString(loginUser.getLoginDto().getPermissions()), jwtConfig.getExpired() + 600);
        redisService.set(RedisCacheEnum.LOGIN_PATH.getPath() + loginUser.getUsername() + RedisCacheEnum.TOKEN_PATH.getPath(),
                token, jwtConfig.getExpired() + 600);

        //删除验证码
        redisService.del(RedisCacheEnum.CAPTCHA_PATH.getPath() + loginUser.getUsername());
        outputStream.write(JSON.toJSONString(HttpResult.success("登录成功", token)).getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

完结某些错误信息 自定义修改即可

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值