SpringBoot + SpringSecurity 와 JWT 인증 및 권한 부여

새로운 지식 을 배 우려 면, 우 리 는 먼저 이 물건 이 무엇 을 하 는 지, 그것 의 장단 점 이 무엇 인지 분석 해 야 한다.
블 로그: jwt 와 session 의 차이 점 과 장점 을 바탕 으로
 
jwt 의 역할 을 알 고 jwt 가 어떻게 생 성 되 었 는 지 살 펴 보 자.
1: 우선 Maven 에 관련 의존 도입


    org.springframework.boot
    spring-boot-starter-security




    io.jsonwebtoken
    jjwt
    0.9.0

2: Jwt 생 성 도구 클래스 (참고)
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author yangfan
 * @description
 *
 * Jwt      
 * JWT   :header.payload.signature
 * header   (  、token   ):
 * {"alg": "HS512","typ": "JWT"}
 * payload   (   、    、    ):
 * {"sub":"wang","created":1489079981393,"exp":1489684781}
 * signature     :
 * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
 *
 * @modified By
 */

@Component
@Slf4j
public class JwtTokenUtil {
    //   
    private static final String CLAIM_KEY_USERNAME = "sub";
    //    
    private static final String CLAIM_KEY_CREATED = "created";
    //jwt  
    @Value("${jwt.secret}")
    private String secret;
    //jwt    
    @Value("${jwt.expiration}")
    private Long expiration;

    /**
     *       JWT token
     *  subject      ,      JSON
     *  issuer        
     *  claims          
     */
    private String generateToken(Map claims) {
        return Jwts.builder()
                //      
                .setClaims(claims)
                //     
                .setExpiration(generateExpirationDate())
                //         
                .signWith(SignatureAlgorithm.HS512, secret)
                //   :    JWT   ,      ,     json      ,      userid,roldid   ,           。
                //.setSubject(subject)
                //    
                //.setIssuer(Optional.ofNullable(issuer).orElse(ISS))
                .compact();
    }

    /**
     *  token   JWT    
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
           log.info("JWT      :{}",token);
        }
        return claims;
    }

    /**
     *   token     
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     *  token        
     */
    public String getUserNameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username =  claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     *   token     
     *
     * @param token             token
     * @param userDetails               
     */
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     *   token      
     */
    private boolean isTokenExpired(String token) {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

    /**
     *  token       
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     *         token
     * @param userDetails       
     *
     */
    public String generateToken(UserDetails userDetails) {
        Map claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    /**
     *   token       
     */
    public boolean canRefresh(String token) {
        return !isTokenExpired(token);
    }

    /**
     *   token
     */
    public String refreshToken(String token) {
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }
}

 
3: 지금 우 리 는 전단 계 정 비밀 번 호 를 통 해 로그 인 합 니 다. 이 로그 인 은 인터페이스 가 위의 jwtTokenUtil 을 통 해 token 을 생 성 합 니 다.
/**
     *            ,       token
     * @param username    
     * @param password   
     * @return String jwt
     */
    @Override
    public String login(String username, String password) {
        String token = null;
        try {
            //                
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            //          ,      BCryptPasswordEncoder   PasswordEncoder  
            if (!passwordEncoder.matches(password, userDetails.getPassword())) {
                throw new BadCredentialsException("     ");
            }
            /**
             * UsernamePasswordAuthenticationToken  AbstractAuthenticationToken  Authentication
             *                        UsernamePasswordAuthenticationToken  (Authentication),
             *      Authentication    AuthenticationManager     
             *  AuthenticationManager      AuthenticationProvider,
             *     Provider   UserDetailsService UserDetail     
             *  UsernamePasswordAuthenticationToken               Authentication
             */
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
            token = jwtTokenUtil.generateToken(userDetails);
        } catch (AuthenticationException e) {
            log.warn("    :{}", e.getMessage());
        }
        return token;
    }

 
4: 위 login 인터페이스 에서 생 성 된 token 을 전단 으로 보 내 고 전단 에서 백 엔 드 인터페이스 에 접근 할 때마다 이 token 을 가지 고 있 습 니 다. 보통 header 머리 에 놓 습 니 다.
이 필 터 는 필터 요청 입 니 다. token 이 유효 하고 합 법 적 인지 검증 한 후에 해당 하 는 사용자 권한 을 가 져 옵 니 다. SecurityContextHolder 에 현재 방문 자의 정 보 를 저장 합 니 다.
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.web.authentication.WebAuthenticationDetailsSource;
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;

/**
 * @author yangfan
 * @description JWT       
 * @modified By
 */
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authHeader = request.getHeader(this.tokenHeader);
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            log.info("checking username:{}", username);
            //SecurityContextHolder              ,                 
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                // SpringSecurityConfig  ,    UserDetailsService   loadUserByUsername  ,
                //         ,    AdminUserDetails   
                // AdminUserDetails      UserDetails  ,     UserDetails    
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                //       token    、     
                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

                    /**
                     *  springsecurity        username password  ,
                     *                ,    WebAuthenticationDetails ,
                     *       HttpServletRequest              
                     *                   
                     */
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    log.info("authenticated user:{}", username);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        filterChain.doFilter(request, response);
    }
}

 
5: 중점 이 왔 습 니 다. 지금부터 spring security 설정 류 를 작성 하려 면 위의 jwt 필 터 를 spring security 에 통합 해 야 합 니 다.
/**
 * @author yangfan
 * @description SpringSecurity   ,  csrf      ,druid         
 * @modified By
 *
 * |@EnableGlobalMethodSecurity(prePostEnabled=true)     ,      controller          
 * |Controller    @PreAuthorize("hasAuthority('pms:brand:read')")  ,               
 * |          ,               Authentication   
 * |SecurityExpressionRoot   getAuthoritySet()               
 *
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UmsAdminService adminService;
    
    //          ,        
    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    
    //      token       ,        
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    /**
     *          url  、jwt            .
     * @param httpSecurity
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .csrf().disable() //  CSRF
                .sessionManagement()//   token,     session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.GET, //                 
                        "/",
                        "/*.html",
                        "/favicon.ico",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/swagger-resources/**",
                        "/v2/api-docs/**"
                )
                .permitAll()
                .antMatchers("/admin/login", "/admin/register")//             
                .permitAll()
                .antMatchers(HttpMethod.OPTIONS)//          options  
                .permitAll()
                .antMatchers("/druid/*")//    druid    ,  CSRF       (Cross—Site Request Forgery)   ,    druid    
                .permitAll()
                .anyRequest()//                  
                .authenticated();
        //     
        httpSecurity.headers().cacheControl();
        //   JWT filter
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        //                 
        httpSecurity.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);
    }

    /**
     *     UserDetailsService PasswordEncoder
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }

    /**
     * SpringSecurity                  ,      BCryptPasswordEncoder
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     *        ,             ,      
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService() {
        //        
        return username -> {
            UmsAdmin admin = adminService.getAdminByUsername(username);
            if (admin != null) {
                List permissionList = adminService.getPermissionList(admin.getId());
                return new AdminUserDetails(admin,permissionList);
            }
            throw new UsernameNotFoundException("        ");
        };
    }

    /**
     *                 ,   jwt token,     token      .
     * @return
     */
    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
        return new JwtAuthenticationTokenFilter();
    }


    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

 
자, 생각 은 바로 이런 생각 입 니 다. 완전한 코드 논 리 를 보고 싶 습 니 다.
전체 코드: 키 즈 입 니 다. 같이 푸 른 달 놀 러 오 세 요.
 

좋은 웹페이지 즐겨찾기