spring Security + jwt使用

目录

(1) jdk版本和springboot版本

(2) 流程说明(可以对照代码实现)

(3) 代码实现

1.spring Security配置WebSecurityConfig

2.jwt身份拦截器JwtAuthenticationTokenFilter

3.自定义身份验证失败处理器类UnauthorizedHandler

4.权限认证失败处理类WAccessDeniedHandler

5.JwtAuthenticationProvider实现AuthenticationProvider接口,进行用户身份验证

6.继承UserDetailsService,从数据库获取用户信息

7.自定义UserDetailsBo类,继承UserDetails

8.自定义权限校验PermissionCheckServiceImpl

9.实现登录接口和接口权限校验

10.登录实现


(1) jdk版本和springboot版本

<properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <springboot.version>3.1.5</springboot.version>
    </properties>

(2) 流程说明(可以对照代码实现)

1.springboot启动时,会先加载WebSecurityConfig配置
(1)WebSecurityConfig里会跳过指定的url【requestMatchers("/auth/login").permitAll()】
(2)增加过滤器【.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)】
(3)绑定认证失败类:exceptionHandling(exce->exce.authenticationEntryPoint(unauthorizedHandler))
(4)绑定权限校验失败类:exceptionHandling(exce->exce.accessDeniedHandler(wAccessDeniedHandler))

2.当/auth/login请求进入时,会先到JwtAuthenticationTokenFilter过滤器,判断请求头中是否有token,因为没有token直接filterChain.doFilter(request, response)下一步,又因为在WebSecurityConfig配置了过滤,不会进入异常类,会直接到达AuthController,
会进入到LoginServiceImpl类,根据用户名和密码进行校验【authenticationManager.authenticate(authentication),真正进行校验的实现类JwtAuthenticationProvider】,校验通过则返回token,
否则抛出异常,返回错误信息。

3.当其它请求进入时,也会先到JwtAuthenticationTokenFilter过滤器,
如果有token,则解析token,获取用户信息,然后设置到SecurityContextHolder中,如果解析失败,则抛出异常,进入异常处理类,返回错误信息。
如果没有token,则会被拦截,进入异常处理类,返回错误信息

(3) 代码实现

1.spring Security配置WebSecurityConfig

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.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
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.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

    @Autowired
    private UnauthorizedHandler unauthorizedHandler;
    @Autowired
    private WAccessDeniedHandler wAccessDeniedHandler;


    /**
     * 认证管理
     * @param configuration
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    /**
     * 认证过滤器
     * @return
     */
    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
        return new JwtAuthenticationTokenFilter();
    }

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

    /**
     * 权限校验
     * @return
     */
    @Bean("per")
    public PermissionCheckServiceImpl permissionCheckServiceImpl(){
        return new PermissionCheckServiceImpl();
    }

    /**
     * 配置安全过滤器链
     * @param httpSecurity
     * @return
     * @throws Exception
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(sessionManager-> sessionManager.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(authorize->authorize
                        .requestMatchers("/auth/login").permitAll()
                        .anyRequest().authenticated())
                .formLogin(Customizer.withDefaults())
                .httpBasic(Customizer.withDefaults())
                //禁用缓存
                .headers(header->header.cacheControl(HeadersConfigurer.CacheControlConfig::disable))
                .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)
                //绑定认证失败类
//                .exceptionHandling(exce->exce.authenticationEntryPoint(unauthorizedHandler))
                //鉴权失败类
                .exceptionHandling(exce->exce.accessDeniedHandler(wAccessDeniedHandler))
                .build();
    }
}

2.jwt身份拦截器JwtAuthenticationTokenFilter

import cn.hutool.core.convert.NumberWithFormat;
import cn.hutool.json.JSONUtil;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import cn.hutool.jwt.JWTValidator;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 *
 * 身份验证拦截器
 */
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException, UsernameNotFoundException {
        //从头中获取token(jwt)
        String authorization = request.getHeader("Authorization");
        //判断token
        if(StringUtils.isBlank(authorization)){
            filterChain.doFilter(request, response);
            return ;
        }
        //校验token格式
        if(!authorization.startsWith("Bearer ")){
            log.error(RespCodeEnum.TOKEN_ERROR.getDesc());
            response(response, RespCodeEnum.TOKEN_ERROR);
            return ;
        }
        //获取jwt数据
        String token = authorization.split(" ")[1];
        if(!JWTUtil.verify(token, AuthConstant.JWT_KEY.getBytes(StandardCharsets.UTF_8))){
            log.error(RespCodeEnum.TOKEN_ERROR.getDesc());
            response(response, RespCodeEnum.TOKEN_ERROR);
        }
        //获取用户名和过期时间
        JWT jwt = JWTUtil.parseToken(token);
        String loginname = (String) jwt.getPayload("loginname");
        //获取jwt中的过期时间
        long exp = ((NumberWithFormat) jwt.getPayload("exp")).longValue();

        //判断是否已经过期
        if(System.currentTimeMillis() / 1000 > exp){
            log.error(RespCodeEnum.TOKEN_EXP.getDesc());
            response(response, RespCodeEnum.TOKEN_EXP);
            return;
        }
        //获取用户信息
        UserDetailsBo userDetails = (UserDetailsBo)userDetailsService.loadUserByUsername(loginname);
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails.getUsername(),
                userDetails.getPassword(), userDetails.getAuthorities());
        authenticationToken.setDetails(userDetails.getUserDto());
        //将认证过了凭证保存到security的上下文中以便于在程序中使用
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request,response);
    }

    private void response(@NotNull HttpServletResponse response,@NotNull RespCodeEnum error) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 或者使用自定义状态码
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(JSONUtil.toJsonStr(ResponseDto.fail(error)));
    }
}

3.自定义身份验证失败处理器类UnauthorizedHandler

import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * 
 * 自定义身份验证失败处理器
 */
@Component
public class UnauthorizedHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        // 设置响应状态码为401(未授权)
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        // 设置响应内容类型
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        // 响应体内容,可以根据需要自定义
        ResponseDto fail = ResponseDto.fail(RespCodeEnum.ACCESS_DENIED);
        response.getWriter().write(JSONUtil.toJsonStr(fail));
    }
}

4.权限认证失败处理类WAccessDeniedHandler

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * 
 * 权限认证失败处理
 */
@Component
public class WAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().print("认证失败");
        response.getWriter().flush();
    }
}

5.JwtAuthenticationProvider实现AuthenticationProvider接口,进行用户身份验证

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * 用户身份验证
 * 
 */
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) {
        String username = String.valueOf(authentication.getPrincipal());
        String password = String.valueOf(authentication.getCredentials());

        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if(userDetails != null && StringUtils.isNotBlank(userDetails.getPassword())
        && userDetails.getPassword().equals(password)){
            return new UsernamePasswordAuthenticationToken(username,password,authentication.getAuthorities());
        }
        throw new BusinessException(RespCodeEnum.NAME_OR_PASSWORD_ERROR);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.equals(authentication);
    }
}

6.继承UserDetailsService,从数据库获取用户信息

import lombok.RequiredArgsConstructor;
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;

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
    private final IUserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserDto user = userService.selectUserByLoginName(username);
        return new UserDetailsBo(user);
    }
}


7.自定义UserDetailsBo类,继承UserDetails

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.stream.Collectors;


@Component
public class UserDetailsBo implements UserDetails {

    private UserDto userDto;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        return userDto.getPermissionName().stream()
                .map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    }

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

    @Override
    public String getUsername() {
        return userDto.getLoginName();
    }

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

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

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

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

    public UserDetailsBo(){}

    public UserDetailsBo(UserDto userDto){
        this.userDto = userDto;
    }

    public UserDto getUserDto() {
        return userDto;
    }

    public void setUserDto(UserDto userDto) {
        this.userDto = userDto;
    }
}

8.自定义权限校验PermissionCheckServiceImpl

import cn.hutool.core.util.ArrayUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.CollectionUtils;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;


public class PermissionCheckServiceImpl {

    public PermissionCheckServiceImpl(){}

    public boolean havePermission(String... permissions)
    {
        if(permissions == null){
            return true;
        }
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if(authentication != null){
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            List<String> authList = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
            for(int i = 0;i < permissions.length;i++){
                if(authList.contains(permissions[i])){
                    return true;
                }
            }
        }
        return false;
    }
}

9.实现登录接口和接口权限校验

import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 认证控制器
 *
 */

@RestController
@RequestMapping("auth")
@RequiredArgsConstructor
public class AuthController {

    private final ILoginService loginService;

    /**
     * 登录
     * @param req 请求参数
     * @return 返回token
     */
    @GetMapping("login")
    public String login(@Validated UserLoginAccPwdDto req) {
        return loginService.loginAccPwd(req);
    }

    @PreAuthorize("@per.havePermission('user','admin')")
    @GetMapping("test")
    public UserInfoVo test() {
        return null;
    }
}

10.登录实现

import cn.hutool.jwt.JWT;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.util.Date;


@Service
@RequiredArgsConstructor
public class LoginServiceImpl implements ILoginService {

    private final AuthenticationManager authenticationManager;

    @Override
    public String loginAccPwd(UserLoginAccPwdDto login) {
        //登录验证
        UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(login.getLoginName(), login.getPassword());
        authenticationManager.authenticate(authentication);
        //生成jwt token
        String token = JWT.create()
                .setPayload("loginname", login.getLoginName())
                .setKey(AuthConstant.JWT_KEY.getBytes(StandardCharsets.UTF_8))
                //过期时间3小时
                .setExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60))
                .sign();
        return token;
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值