Spring SecurityとJWTの組み合わせ

16984 ワード

概要
  • JWTを使用して権限検証を行うメリットは、Sessionに比べてサーバメモリを大量に消費する必要があり、マルチサーバ時に共有Sessionの問題が発生し、携帯電話などのモバイル端末へのアクセスが面倒な
  • であることです.
  • でJWTはサーバに格納する必要がなく、サーバリソースを占有しない(すなわち無状態である)、ユーザはログイン後にTokenを取得した後、アクセスに必要な権限の要求時にToken(一般的にHttp要求ヘッダに設定されている)を添付し、JWTはマルチサーバ共有の問題もなく、携帯電話のモバイル側アクセスの問題もなく、セキュリティを向上させるためには、TokenをユーザのIPアドレスにバインドできるケースソースコードダウンロード
  • フロントエンドプロセス
  • ユーザがAJAXによりログインしてToken
  • を得る.
  • 以降アクセスに権限要求が必要な場合はTokenを添付して
  • にアクセスする.
    
    
    
        
        Title
        
        
            var header = "";
            function login() {
                $.post("http://localhost:8080/auth/login", {
                    username: $("#username").val(),
                    password: $("#password").val()
                }, function (data) {
                    console.log(data);
                    header = data;
                })
            }
            function toUserPageBtn() {
                $.ajax({
                    type: "get",
                    url: "http://localhost:8080/userpage",
                    beforeSend: function (request) {
                        request.setRequestHeader("Authorization", header);
                    },
                    success: function (data) {
                        console.log(data);
                    }
                });
            }
        
    
    
        
    Please Login

    バックエンドプロセス(Spring Boot+Spring Security+JJJWT)
    考え方:
  • ユーザ、パーミッションエンティティクラス、およびデータ転送オブジェクト
  • を作成する.
  • は、ユーザ情報
  • を取得するためのDao層インタフェースを記述する.
  • UserDetails(Securityがサポートするユーザエンティティオブジェクト、権限情報を含む)
  • を実装する.
  • UserDetailsSevice(データベースからユーザ情報を取得し、UserDetailsにパッケージ)
  • を実現する.
  • JWTToken生成ツールを記述し、Token
  • を生成、検証、解析する
  • 設定Security、構成要求処理と設定UserDetails取得方式がカスタムのUserDetailsSevice
  • である.
  • LoginControllerを作成し、ユーザーのログイン名パスワードを受信して検証を行い、検証に成功したらTokenに戻ってユーザー
  • に与える.
  • フィルタを作成し、ユーザ要求ヘッダまたはパラメータにTokenが含まれている場合に解析し、Authenticationを生成し、SecurityContextにバインドし、Securityが
  • を使用するようにする.
  • ユーザは、権限を必要とするページにアクセスしたが、正しいTokenが添付されておらず、フィルタ処理時にAuthenticationが生成されず、アクセス権限も存在せず、アクセスできず、Noアクセス成功
  • .
    ユーザーエンティティクラスを作成し、データを挿入します.
    User(ユーザ)エンティティークラス
    @Data
    @Entity
    public class User {
        @Id
        @GeneratedValue
        private int id;
        private String name;
        private String password;
        @ManyToMany(cascade = {CascadeType.REFRESH}, fetch = FetchType.EAGER)
        @JoinTable(name = "user_role", joinColumns = {@JoinColumn(name = "uid", referencedColumnName = "id")}, inverseJoinColumns = {@JoinColumn(name = "rid", referencedColumnName = "id")})
        private List roles;
    } 
    

    ロールエンティティクラス
    @Data
    @Entity
    public class Role {
        @Id
        @GeneratedValue
        private int id;
        private String name;
        @ManyToMany(mappedBy = "roles")
        private List users;
    }
    

    データの挿入
    Userテーブル
    id
    name
    password
    1
    linyuan
    123
    ロールテーブル
    id
    name
    1
    USER
    User_ROLE表
    uid
    rid
    1
    1
    Daoレイヤインタフェース、ユーザー名でデータを取得し、Java 8のOptionalオブジェクトを返す
    public interface UserRepository extends Repository {
        Optional findByName(String name);
    }
    

    フロントエンドとのデータ転送のためのLoginDTOの作成
    @Data
    public class LoginDTO implements Serializable {
        @NotBlank(message = "       ")
        private String username;
        @NotBlank(message = "      ")
        private String password;
    }
    

    Token生成ツールを作成し、JJWTライブラリを使用して作成します.Tokenの生成(Stringに戻る)、Tokenの解析(Authentication認証オブジェクトに戻る)、Tokenの検証(ブール値に戻る)の3つの方法があります.
    @Component
    public class JWTTokenUtils {
    
        private final Logger log = LoggerFactory.getLogger(JWTTokenUtils.class);
    
        private static final String AUTHORITIES_KEY = "auth";
    
        private String secretKey;           //    
    
        private long tokenValidityInMilliseconds;       //    
    
        private long tokenValidityInMillisecondsForRememberMe;      //(   )    
    
        @PostConstruct
        public void init() {
            this.secretKey = "Linyuanmima";
            int secondIn1day = 1000 * 60 * 60 * 24;
            this.tokenValidityInMilliseconds = secondIn1day * 2L;
            this.tokenValidityInMillisecondsForRememberMe = secondIn1day * 7L;
        }
    
        private final static long EXPIRATIONTIME = 432_000_000;
    
        //  Token
        public String createToken(Authentication authentication, Boolean rememberMe){
            String authorities = authentication.getAuthorities().stream()       //          ,  USER,ADMIN
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.joining(","));
    
            long now = (new Date()).getTime();              //       
            Date validity;                                          //      
            if (rememberMe){
                validity = new Date(now + this.tokenValidityInMilliseconds);
            }else {
                validity = new Date(now + this.tokenValidityInMillisecondsForRememberMe);
            }
    
            return Jwts.builder()                                   //  Token  
                    .setSubject(authentication.getName())           //      
                    .claim(AUTHORITIES_KEY,authorities)             //      
                    .setExpiration(validity)                        //      
                    .signWith(SignatureAlgorithm.HS512,secretKey)   //    
                    .compact();
        }
    
        //      
        public Authentication getAuthentication(String token){
            System.out.println("token:"+token);
            Claims claims = Jwts.parser()                           //  Token payload
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token)
                    .getBody();
          
            Collection extends GrantedAuthority> authorities =
                    Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))         //         
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());                                                  //      GrantedAuthority    
    
            User principal = new User(claims.getSubject(), "", authorities);
            return new UsernamePasswordAuthenticationToken(principal, "", authorities);
        }
    
        //  Token    
        public boolean validateToken(String token){
            try {
                Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);   //      Token
                return true;
            }catch (SignatureException e) {                                     //    
                log.info("Invalid JWT signature.");
                log.trace("Invalid JWT signature trace: {}", e);
            } catch (MalformedJwtException e) {                                 //JWT    
                log.info("Invalid JWT token.");
                log.trace("Invalid JWT token trace: {}", e);
            } catch (ExpiredJwtException e) {                                   //JWT  
                log.info("Expired JWT token.");
                log.trace("Expired JWT token trace: {}", e);
            } catch (UnsupportedJwtException e) {                               //    JWT
                log.info("Unsupported JWT token.");
                log.trace("Unsupported JWT token trace: {}", e);
            } catch (IllegalArgumentException e) {                              //      
                log.info("JWT token compact of handler are invalid.");
                log.trace("JWT token compact of handler are invalid trace: {}", e);
            }
            return false;
        }
    }
    

    ユーザーエンティティークラスを代表するUserDetailsインタフェースを実現し、私たちのUserオブジェクトにパッケージを行い、権限などの性質を含み、Spring Securityで使用することができます.
    public class MyUserDetails implements UserDetails{
    
        private User user;
    
        public MyUserDetails(User user) {
            this.user = user;
        }
    
        @Override
        public Collection extends GrantedAuthority> getAuthorities() {
            List roles = user.getRoles();
            List authorities = new ArrayList<>();
            StringBuilder sb = new StringBuilder();
            if (roles.size()>=1){
                for (Role role : roles){
                    authorities.add(new SimpleGrantedAuthority(role.getName()));
                }
                return authorities;
            }
            return AuthorityUtils.commaSeparatedStringToAuthorityList("");
        }
    
        @Override
        public String getPassword() {
            return user.getPassword();
        }
    
        @Override
        public String getUsername() {
            return user.getName();
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    

    UserDetailsを取得する方法は1つしかありません.データベースからUserオブジェクトを取得し、UserDetailsにパッケージして戻ります.
    @Service
    public class MyUserDetailsService implements UserDetailsService {
        @Autowired
        UserRepository userRepository;
    
        @Override
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
            //           
            Optional user = userRepository.findByName(s);
            //   ,               
            user.ifPresent((value)->System.out.println("   :"+value.getName()+"     :"+value.getPassword()));
            //       null
            return new MyUserDetails(user.orElse(null));
        }
    }
    

    ユーザーがTokenを携帯するとTokenを取得し、Tokenに基づいてAuthentication認証オブジェクトを生成し、Spring Securityの権限制御のためにSecurityContextに保存するフィルタを作成します.
    public class JwtAuthenticationTokenFilter extends GenericFilterBean {
    
        private final Logger log = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
    
        @Autowired
        private JWTTokenUtils tokenProvider;
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("JwtAuthenticationTokenFilter");
            try {
                HttpServletRequest httpReq = (HttpServletRequest) servletRequest;
                String jwt = resolveToken(httpReq);
                if (StringUtils.hasText(jwt) && this.tokenProvider.validateToken(jwt)) {            //  JWT    
                    Authentication authentication = this.tokenProvider.getAuthentication(jwt);      //        
                    SecurityContextHolder.getContext().setAuthentication(authentication);           //      SecurityContext
                }
                filterChain.doFilter(servletRequest, servletResponse);
            }catch (ExpiredJwtException e){                                     //JWT  
                log.info("Security exception for user {} - {}",
                        e.getClaims().getSubject(), e.getMessage());
    
                log.trace("Security exception trace: {}", e);
                ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            }
        }
    
        private String resolveToken(HttpServletRequest request){
            String bearerToken = request.getHeader(WebSecurityConfig.AUTHORIZATION_HEADER);         // HTTP    TOKEN
            if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")){
                return bearerToken.substring(7, bearerToken.length());                              //  Token   ,  Bearer
            }
            String jwt = request.getParameter(WebSecurityConfig.AUTHORIZATION_TOKEN);               //        TOKEN
            if (StringUtils.hasText(jwt)) {
                return jwt;
            }
            return null;
        }
    }
    

    LoginControllerを作成し、ユーザーはユーザー名、パスワードを通じて/auth/loginにアクセスし、LoginDTOオブジェクトを通じて受信し、Authenticationオブジェクトを作成し、コードの中でU s e r m e P a sswordAuthenticationTokenであり、オブジェクトが存在するかどうかを判断し、AuthenticationManagerのauthenticateメソッドを通じて認証オブジェクトを検証する.AuthenticationManagerの実装クラスProviderManagerはAuthentionProvider(認証処理)で検証され、デフォルトのProviderManagerはDaoAuthenticationProviderを呼び出して認証処理を行い、DaoAuthenticationProviderではUserDetailsService(認証情報ソース)でUserDetailsを取得します.認証が成功すると、権限を含むAuthentionが返され、SecurityContextHolder.getContext().setAuthentication()がSecurityContextに設定され、Authenticationに基づいてTokenが生成され、ユーザーに返されます.
    @RestController
    public class LoginController {
    
        @Autowired
        private UserRepository userRepository;
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private JWTTokenUtils jwtTokenUtils;
    
        @RequestMapping(value = "/auth/login",method = RequestMethod.POST)
        public String login(@Valid LoginDTO loginDTO, HttpServletResponse httpResponse) throws Exception{
            //             Authentication     ,     UsernamePasswordAuthenticationToken
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDTO.getUsername(),loginDTO.getPassword());
            //         
            if (Objects.nonNull(authenticationToken)){
                userRepository.findByName(authenticationToken.getPrincipal().toString())
                        .orElseThrow(()->new Exception("     "));
            }
            try {
                //   AuthenticationManager(     ProviderManager) authenticate     Authentication   
                Authentication authentication = authenticationManager.authenticate(authenticationToken);
                //  Authentication     SecurityContext
                SecurityContextHolder.getContext().setAuthentication(authentication);
                //  Token
                String token = jwtTokenUtils.createToken(authentication,false);
                // Token   Http  
                httpResponse.addHeader(WebSecurityConfig.AUTHORIZATION_HEADER,"Bearer "+token);
                return "Bearer "+token;
            }catch (BadCredentialsException authentication){
                throw new Exception("    ");
            }
        }
    }
    

    Security構成クラスを作成し、WebSecurityConfigurerAdapterを継承し、configureメソッドを書き換える
    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        public static final String AUTHORIZATION_HEADER = "Authorization";
    
        public static final String AUTHORIZATION_TOKEN = "access_token";
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth
                    //         
                    .userDetailsService(userDetailsService)
                    //      
                    .passwordEncoder(passwordEncoder());
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //        
            http
                    //  CSRF、CORS
                    .cors().disable()
                    .csrf().disable()
                    //    Token,     Session
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                    //  Http  
                    .authorizeRequests()
                    //               
                    .antMatchers("/","/auth/login").permitAll()
                    //              
                    .anyRequest().authenticated()
                    //          
                    .antMatchers("/userpage").hasAnyRole("USER")
                    .and()
                    //    
                    .logout().permitAll();
            //  JWT filter  
            http
                    .addFilterBefore(genericFilterBean(), UsernamePasswordAuthenticationFilter.class);
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        public GenericFilterBean genericFilterBean() {
            return new JwtAuthenticationTokenFilter();
        }
    }
    

    テスト用のControllerの作成
    @RestController
    public class UserController {
    
        @PostMapping("/login")
        public String login() {
            return "login";
        }
    
        @GetMapping("/")
        public String index() {
            return "hello";
        }
    
        @GetMapping("/userpage")
        public String httpApi() {
            System.out.println(SecurityContextHolder.getContext().getAuthentication().getPrincipal());
            return "userpage";
        }
    
        @GetMapping("/adminpage")
        public String httpSuite() {
            return "userpage";
        }
    
    }