Spring Securityプロジェクト権限設計過程解析


なぜSpring Securityを選んだのですか?
今はJava Webの世界でSpringは一統江湖と言えます。マイクロサービスが来るにつれて、SpringCloudはJavaプログラマが熟知しなければならない枠組みと言えます。アリもSpringCloudのためにソースを書いています。例えば有名なNacosはSpringの実の息子として、Spring Securityはマイクロサービスの生態によく適応しました。Oauthと連携して認証センターサービスを行うことができます。本文はまず一番簡単な単体プロジェクトからスタートして、Securityを逐次把握します。詳細は公式文書に達することができます。
準備
簡単なデモを用意しました。具体的なコードは文末に入れます。事前に声明しましたが、本demoはJWTを使っていませんでした。tokenのメンテナンスをサーバーに置いて、期限が過ぎた時間をより良いメンテナンスしたいからです。もちろん、将来のマイクロサービス認証センターの形式なら、JWTも便利なメンテナンス期限が切れています。あまり議論しないでください。
本プロジェクトの構造は以下の通りです。

また、本デモはMybatis Plus、lombookを使用しています。
コアコード
まず二つのクラスを実現する必要があります。一つはUserDetailの実現クラスSecurityUserで、一つはUserDetails Serviceの実現類SecurityUserServiceです。

**
 * Security        User 
 * */
@Data
public class SecurityUser implements UserDetails {
 @Autowired
 private SysRoleService sysRoleService;
 //     (     username SysUser loginName    )
 private String username;
 //    
 private String password;
 //  id
 private SysUser sysUser;
 //        
 private List<SysMenu> sysMenuList;
 /**    */
 public SecurityUser(SysUser sysUser){
  this.username = sysUser.getLoginName();
  this.password = sysUser.getPassword();
  this.sysUser = sysUser;
 }
 public SecurityUser(SysUser sysUser,List<SysMenu> sysMenuList){
  this.username = sysUser.getLoginName();
  this.password = sysUser.getPassword();
  this.sysMenuList = sysMenuList;
  this.sysUser = sysUser;
 }
 /**       */
 @Override
 public Collection<? extends GrantedAuthority> getAuthorities() {
  List<GrantedAuthority> authorities = new ArrayList<>();
  for(SysMenu menu : sysMenuList) {
   authorities.add(new SimpleGrantedAuthority(menu.getPerms()));
  }
  return authorities;
 }
 @Override
 public String getPassword() {
  return this.password;
 }
 @Override
 public String getUsername() {
  return this.username;
 }
 //       
 @Override
 public boolean isAccountNonExpired() {
  return true;
 }
 //        
 @Override
 public boolean isAccountNonLocked() {
  return true;
 }
 //        
 @Override
 public boolean isCredentialsNonExpired() {
  return true;
 }
 //      
 @Override
 public boolean isEnabled() {
  return true;
 }
}
このクラスにはある依頼者の情報が含まれています。Securityでは主体といいます。この方法は必須であり、ユーザの特定の権限を取得することができる。こちらの権限の粒子度はメニューレベルに達しています。オープンソースの多くの中のキャラクターのレベルではなく、粒度が細かいほど便利だと思います。

/**
 * Security        UserService 
 * */
@Service
public class SecurityUserService implements UserDetailsService{

 @Autowired
 private SysUserService sysUserService;
 @Autowired
 private SysMenuService sysMenuService;
 @Autowired
 private HttpServletRequest httpServletRequest;

 @Override
 public SecurityUser loadUserByUsername(String loginName) throws UsernameNotFoundException {
  LambdaQueryWrapper<SysUser> condition = Wrappers.<SysUser>lambdaQuery().eq(SysUser::getLoginName, loginName);
  SysUser sysUser = sysUserService.getOne(condition);
  if (Objects.isNull(sysUser)){
   throw new UsernameNotFoundException("      !");
  }
  Long projectId = null;
  try{
   projectId = Long.parseLong(httpServletRequest.getHeader("projectId"));
  }catch (Exception e){

  }
  SysMenuModel sysMenuModel;
  if (sysUser.getUserType()){
   sysMenuModel = new SysMenuModel();
  }else {
   sysMenuModel = new SysMenuModel().setUserId(sysUser.getId());
  }
  sysMenuModel.setProjectId(projectId);
  List<SysMenu> menuList = sysMenuService.getList(sysMenuModel);
  return new SecurityUser(sysUser,menuList);
 }
}
このクラスは、あるユーザの権限をすべて取得し、本体を生成し、後のfilterで彼の役割を見ることができる唯一の方法loadUserByUsernameを実現することが明らかになっている。
配置とfilterを見る前に、このような提供方法は、ユーザがログインしていない、またはtokenが失効した場合に統一的に戻すことができるというクラスの説明が必要です。

@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {

 private static final long serialVersionUID = 1L;

 @Override
 public void commence(HttpServletRequest request, HttpServletResponse response,
       AuthenticationException authException) throws IOException, ServletException {
  response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"token  ,      ");
 }
}
ok,次に配置を見て,WebSecurityConfigrer AdapterのSecurityConfig類を実現しました。特に説明します。本demoは前後の端分離を前提に書かれていますので、多すぎる方法を実現しています。この類は三つの方法を実現できます。

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

 @Autowired
 SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;
 @Autowired
 SecurityFilter securityFilter;

 @Override
 protected void configure(HttpSecurity http) throws Exception {
  http
    //  csrf
    .csrf().disable()
    //    
    .exceptionHandling().authenticationEntryPoint(securityAuthenticationEntryPoint).and()
    //Session    
    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
    //    
    .authorizeRequests()
    .antMatchers("/login/login").permitAll()
    .antMatchers("/login/register").permitAll()
    .antMatchers("/login/logout").permitAll()
    .anyRequest().authenticated();
  http
    .addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class);
 }
}
異常処理は上のような種類です。Sessionのいくつかの管理方式は私もあのSecurity+JWTの文章の中で説明しました。比較的簡単です。そして、いくつかの検証していない登録経路です。残りは全部私達の下のfilterを通ります。

@Slf4j
@Component
public class SecurityFilter extends OncePerRequestFilter {

 @Autowired
 SecurityUserService securityUserService;
 @Autowired
 SysUserService sysUserService;
 @Autowired
 SysUserTokenService sysUserTokenService;

 /**
  *     
  * */
 @Override
 protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
         FilterChain filterChain) throws ServletException, IOException {
  log.info("      :{}",httpServletRequest.getRequestURL());
  try {
   final String token = httpServletRequest.getHeader("token");
   LambdaQueryWrapper<SysUserToken> condition = Wrappers.<SysUserToken>lambdaQuery().eq(SysUserToken::getToken, token);
   SysUserToken sysUserToken = sysUserTokenService.getOne(condition);
   if (Objects.nonNull(sysUserToken)){
    SysUser sysUser = sysUserService.getById(sysUserToken.getUserId());
    if (Objects.nonNull(sysUser)){
     SecurityUser securityUser = securityUserService.loadUserByUsername(sysUser.getLoginName());
     //       
     UsernamePasswordAuthenticationToken authentication =
       new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
     authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
     //      
     SecurityContextHolder.getContext().setAuthentication(authentication);
    }
   }
  }catch (Exception e){
   log.error("       :{}", Arrays.toString(e.getStackTrace()));
  }
  filterChain.doFilter(httpServletRequest, httpServletResponse);
 }
}
ユーザーがログインしているかどうかを判断すると、データベースから期限が切れていないtokenがあるかどうかを確認し、存在する場合は、プロジェクトのメモリに本体情報を入れます。
以上でSecurityコアの作成を完了しました。業務コードのためにメモリ内の本体情報を入手しやすくするために、ユーザー情報を取得する方法を追加しました。

/**
 *   Security     
 * @author pjjlt
 * */
public class SecurityUserUtil {
 public static SysUser getCurrentUser(){
  SecurityUser securityUser = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
  if (Objects.nonNull(securityUser) && Objects.nonNull(securityUser.getSysUser())){
   return securityUser.getSysUser();
  }
  return null;
 }
}
ビジネスコード
以上はSecurityコアコードで、以下は簡単に二つの業務コードを追加します。例えば登録とあるインターフェースの権限アクセステストです。
万物の源登録
まず、filterにブロックされない3つの方法で登録、ログイン、ログインして、私はすべてmoude.co ntroller.Login Controllerという経路の下で書いています。登録はもちろん、insertUserの方法です。判断をよくしてください。パスワードはAESを通して密にしてください。
下は登録コードを見てください。controller層は言わないです。とにかく参考になります。

 /**
  *   ,      ,      
  * */
 @Override
 @Transactional(rollbackFor = Exception.class)
 public JSONObject login(SysUserModel sysUserModel) throws Exception{
  JSONObject result = new JSONObject();
  //1.         、      、      
  Wrapper<SysUser> sysUserWrapper = Wrappers.<SysUser>lambdaQuery()
    .eq(SysUser::getLoginName,sysUserModel.getLoginName()).or()
    .eq(SysUser::getEmail,sysUserModel.getEmail());
  SysUser sysUser = baseMapper.selectOne(sysUserWrapper);
  if (Objects.isNull(sysUser)){
   throw new Exception("     !");
  }
  String password = CipherUtil.encryptByAES(sysUserModel.getPassword());
  if (!password.equals(sysUser.getPassword())){
   throw new Exception("     !");
  }
  if (sysUser.getStatus()){
   throw new Exception("         !");
  }
  // 2.        
  sysUser.setLoginIp(ServletUtil.getClientIP(request));
  sysUser.setLoginDate(LocalDateTime.now());
  baseMapper.updateById(sysUser);
  // 3.  token,    
  String token = UUID.fastUUID().toString().replace("-","");
  LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireTimeSeconds);
  SysUserToken sysUserToken = new SysUserToken()
    .setToken(token).setUserId(sysUser.getId()).setExpireTime(expireTime);
  sysUserTokenService.save(sysUserToken);
  result.putOpt("token",token);
  result.putOpt("expireTime",expireTime);
  return result;
 }
まず、ユーザーが存在するかどうかを確認して、ログインパスワードが正しいかを確認してから、tokenをカプセル化します。ちなみに、私はデータベースからユーザーが登録したtokenを取得していません。その後、有効期限を更新して、ログインするたびに新しいtokenを取得します。後期はアカウント登録数の制御もできます。
そして、登録して、ライブラリの中にあるtokenを削除します。

 /**
  *   ,  token
  * */
 @Override
 public void logout() throws Exception{
  String token = httpServletRequest.getHeader("token");
  if (Objects.isNull(token)){
   throw new LoginException("token   ",ResultEnum.LOGOUT_ERROR);
  }
  LambdaQueryWrapper<SysUserToken> sysUserWrapper = Wrappers.<SysUserToken>lambdaQuery()
    .eq(SysUserToken::getToken,token);
  baseMapper.delete(sysUserWrapper);
 }
パーミッションの検証
こちらは二つのアカウントを維持しました。一つはスーパー管理者のmajianで、すべての権限を持っています。一つは普通の人です。pjltはいくつかの権限しかありません。アクセスインターフェースの効果を見てみます。
私たちが訪問したインターフェースはmoude.co ntroller.Login Controllerの経路の下です。

@PreAuthorize("hasAnyAuthority('test')")
@GetMapping("test")
public String test(){
 return "test";
}
その中のハスAnyAuthoritはパーミッションコードです。
私達は異なるアカウントでのアクセスをシミュレーションします。つまり、変更要求のheaderの中のtoken値は、ログイン段階がフロントエンドに戻るtokenです。
まずはスーパー管理者認証です。

そして一般管理者がアクセスします。

ログインしていません(tokenが存在しないか、または期限が切れています)。

demoアドレス
https://github.com/majian1994/easy-file-back
結尾語
この文章は簡単に説明しました。主にSecurity関連のものを具体的にキャラクターの三つの要素を実現します。ユーザー、キャラクター、権限(メニュー)は私のコードを見てもいいです。全部テスト済みです。文書管理システムを書きたいです。もっと良い管理インターフェース文書を作成してください。だからこのコードは私の小さいデモになりました。
以上が本文の全部です。皆さんの勉強に役に立つように、私たちを応援してください。