JDK17使用security

JDK17使用security

无咎 26 2023-04-07

SpringBoot3.x + Jdk17 + Security + Jwt

gradle中引入相应的jar包

    // security
    implementation 'org.springframework.boot:spring-boot-starter-security'
    // jwt
    implementation 'io.jsonwebtoken:jjwt:0.9.1'
    implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359'

    // knife4j接口文档
    implementation 'com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:4.1.0'

编写各种处理器(如果前后端分离可以不用)

// 登录失败

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@Slf4j
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e)
            throws IOException {
        log.info("登陆失败");
        //修改状态码
        httpServletResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        httpServletResponse.setContentType("application/json;charset=utf-8");
        httpServletResponse.getWriter().write(objectMapper.writeValueAsString(e.getMessage()));
    }

}
// 登录成功

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@Slf4j
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication)
            throws IOException {
        log.info("登陆成功,{}", authentication);

        httpServletResponse.setStatus(HttpStatus.OK.value());
        httpServletResponse.setContentType("application/json;charset=utf-8");
        httpServletResponse.getWriter().write("登陆成功");
    }

}
// 重复登录

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
@Slf4j
public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        Map<String,Object> map=new HashMap<>();
        map.put("code",0);
        map.put("msg","该账号在另一处登录,您被迫下线:"+event.getSessionInformation().getLastRequest());

        //map转json
        String json=objectMapper.writeValueAsString(map);

        event.getResponse().setContentType("application/json;charset=utf-8");
        event.getResponse().getWriter().write(json);

        // 如果是跳转html页面,url代表跳转的地址
        // redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), "url");
    }

}
// 踢下线

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@Slf4j
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication)
            throws IOException {
        String username=((User)authentication.getPrincipal()).getUsername();
        log.info("退出成功,用户名:{}",username);

        httpServletResponse.setContentType("application/json;charset=utf-8");
        httpServletResponse.getWriter().write("退出成功");
    }

}

编写JWT配置文件

// JWT 配置

import com.springboot.tdm.user.service.UserDetailsServiceImpl;
import com.springboot.tdm.utils.JwtTokenUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Slf4j
@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    public static final String BEARER = "Bearer ";

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    /**
     * 从 Authorization 标头中,提取令牌
     *
     */
    private String parseJwt(HttpServletRequest request) {
        String headerAuth = request.getHeader("Authorization");

        if (StringUtils.hasText(headerAuth) && headerAuth.startsWith(BEARER)) {
            return headerAuth.substring(BEARER.length());
        }
        return null;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String jwt = parseJwt(request);
            if (jwt != null && jwtTokenUtil.validateJwtToken(jwt)) {
                String username = jwtTokenUtil.getUsernameFromJwtToken(jwt);
                // 如果令牌存在,则加载令牌
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        } catch (Exception e) {
            log.error("无法设置用户认证:{}", e.getMessage());
        }
        filterChain.doFilter(request, response);
    }

}
// JWT工具类

import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import java.util.Date;

@Configuration
@Component
@Slf4j
public class JwtTokenUtil {

    /**
     * token的头key
     */
    public static final String TOKEN_HEADER = "Authorization";

    /**
     * token前缀
     */
    public static final String TOKEN_PREFIX = "Bearer ";

    /**
     * token 过期时间 30分钟
     */
    public static final long EXPIRATION = 1000 * 60 * 30;

    /**
     * 加密的key
     */
    public static final String APP_SECRET_KEY = "secret";

    /**
     * 生成token
     *
     * @param username 用户名
     * @return token
     */
    public static String createToken(String username) {

        String token = Jwts
                .builder()
                .setSubject(username)
                //.setClaims(map)
                // .claim("username", username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(SignatureAlgorithm.HS256, APP_SECRET_KEY)
                .compact();

        return TOKEN_PREFIX +token;
    }


    /**
     * 获取当前登录用户用户名
     */
    public static String getUsername(String token) {
        Claims claims = Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();
        return claims.get("username").toString();
    }

    /**
     * 获取当前登录用户角色
     */
    public static String getUserRole(String token) {
        Claims claims = Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();
        return claims.get("role").toString();
    }


    /**
     * 检查token是否过期
     */
    public static boolean isExpiration(String token) {
        try {
            Claims claims = Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();
            return claims.getExpiration().before(new Date());
        } catch (Exception e) {
            return true;
        }
    }


    public boolean validateJwtToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException e) {
            log.error("Invalid JWT signature: {}", e.getMessage());
        } catch (MalformedJwtException e) {
            log.error("Invalid JWT token: {}", e.getMessage());
        } catch (ExpiredJwtException e) {
            log.error("JWT token is expired: {}", e.getMessage());
        } catch (UnsupportedJwtException e) {
            log.error("JWT token is unsupported: {}", e.getMessage());
        } catch (IllegalArgumentException e) {
            log.error("JWT claims string is empty: {}", e.getMessage());
        }
        return false;
    }

    public String getUsernameFromJwtToken(String token) {
        return Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody().getSubject();
    }

}

Security配置类

import com.springboot.tdm.security.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
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;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private CustomAuthenticationSuccessHandler successHandler;

    @Autowired
    private CustomAuthenticationFailureHandler failureHandler;

    // @Autowired
    // private CustomLogoutSuccessHandler logoutSuccessHandler;

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    //主动踢出用户
    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
        http.authorizeHttpRequests((authorize)->authorize
                        // 放行接口
                        .requestMatchers("/doc.html", "/webjars/**", "/v3/api-docs/**").permitAll()
                        .requestMatchers("/admin/auth", "/frontend/**", "/android/**").permitAll()
                        // .requestMatchers("/admin/**", "/frontend/**").permitAll()
                        // 其它页面需要授权才可以访问。
                        .anyRequest().authenticated())
                // .formLogin(form->form
                //         // 自定义登录
                //         .loginProcessingUrl("/api/v1/admin/auth")
                //         // .loginProcessingUrl("/admin/auth")
                //         // 登录成功
                //         .successHandler(successHandler)
                //         // 登录失败
                //         .failureHandler(failureHandler)
                //         .permitAll())
                // 没有认证时,在这里处理结果,不要重定向
                .exceptionHandling()
		// 不能加,加了如果后端错误前端永远都只有这里的信息
                // .authenticationEntryPoint((req, res, authException) -> {
                //     res.setContentType("application/json;charset=utf-8");
                //     res.setStatus(401);
                //     PrintWriter out = res.getWriter();
                //     out.write(new ObjectMapper().writeValueAsString("用户未登录"));
                //     out.flush();
                //     out.close();
                // })
		.and()
                // 添加跨域配置
                .cors().and()
                .csrf().disable()
                // // 退出登录
                // .logout()
                // // 退出接口
                // .logoutUrl("/logout")
                // // 清楚浏览器的 JSESSIONID 数据
                // .deleteCookies("JSESSIONID")
                // // 退出后的处理
                // .logoutSuccessHandler(logoutSuccessHandler)
                // .permitAll()
                // .and()
                // 不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    //密码加密用的,不然没法做密码比对。
    @Bean
    public PasswordEncoder getPwdEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
        configuration.addExposedHeader("Authorization");
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:8078", "http://127.0.0.1:8078"));
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

}

重写 loadUserByUsername 方法

import com.springboot.tdm.user.model.entity.RouterRelationDO;
import com.springboot.tdm.user.model.repository.RouterRelationRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

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

@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {

    private final RouterRelationRepository relationRepository;

    public UserDetailsServiceImpl(RouterRelationRepository relationRepository) {
        this.relationRepository = relationRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        RouterRelationDO user = relationRepository.checkLogin(username);
        if(user == null){
            throw new UsernameNotFoundException("用户不存在");
        }

        String pwd = new BCryptPasswordEncoder().encode(user.getUid().getPwd());

        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
        //角色前一定要加上"ROLE_"前缀,否则SpringSecurity会视为无效,它是通过这个前缀识别角色的。
        SimpleGrantedAuthority rolesAuthority = new SimpleGrantedAuthority("ROLE_".concat(user.getUid().getRole().name()));
        grantedAuthorityList.add(rolesAuthority);
        grantedAuthorityList.add(new SimpleGrantedAuthority("ROLE_USER"));
        return new User(username, pwd, grantedAuthorityList);
    }

}

Service层中调用并生成token

            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            String token = JwtTokenUtil.createToken(username);

在Springboot启动类中配置注解

@EnableMethodSecurity

在Controller中配置访问权限

@PreAuthorize("hasAnyRole('SUPPER', 'ADMIN')")