序文
同社は最近、認証および承認のための春のセキュリティフレームワークを使用して、新しいプロジェクトを作りました。それが完了するまでに3日かかりました:
- アカウントパスワード
- JWT
- URLレベルのアクセス
実際には、それはまたに使用されているプロジェクトが、基本的にはCtrl + C、Ctrl + Vの前に。あまりにもこのプロセスの全体的な枠組みを理解していない、それは霧であるが、実際には非常に正常走りました。
私は前にいくつかのオンラインチュートリアルを見てきましたが、あまりにも理解して見ていない、彼らは最も重要な事についてものの、いくつかの重要度の低いについて話しました、とされているので、それはネクサスポイントが言及することはできませんので、参照しますか、比較的無知、一般的に私は理解しますが、私が書きたい、私はしません!だから、Sidongfeidong状態で、彼はノーリターンのこの道に乗り出しました。
ADOは、ポイントを取得します!!!
事前知識
スプリングセキュリティがフィルタに対応する機能のシリーズを介して達成され、各フィルタは、一つの機能に対応します。各フィルタは次のフィルタに要求をフィルタリングするだけでフィルターを通して、元のデータ、またはを変更する可能性があるので、したがって、この場合には、フィルタの順序は、非常に重要です。これは、セキュリティの基本原則の春です。あなたがたは、かなりシンプルなように見えます。
フィルタチェーン
これは私が6,7は、カスタムされたプロジェクトで使用されるフィルタ全体の鎖です。6:、フィルターパスワードアカウントです7:検証JWT、12のためのフィルタです:フィルタのチェック許可
アカウントパスワード
アカウントのパスワードは、ほぼすべてのプロジェクトは、機能を持っているでしょう。
フィルターコード
public class CustomUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
CustomUsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
String body = StreamUtils.copyToString(httpServletRequest.getInputStream(), Charset.forName("UTF-8"));
String username = null;
String password = null;
if (StringUtils.hasText(body)){
JSONObject jsonObject = JSON.parseObject(body);
username = jsonObject.getString("username");
password = jsonObject.getString("password");
}
if (username == null){
username = "";
}
if (password == null){
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
复制代码
コードを分析してみましょう:
まず、フィルタクラスは、実際に普通のフィルターである、抽象クラスのフィルタを継承するが、彼はそれにいくつかのことをやった、と我々は我々限り、特にやっに使用されるこれらの事を知っている必要はありません。それが知られている含まれているdoFilter
方法を
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
复制代码
実装場所を私たちは見ることができattemptAuthentication
、この方法では、このメソッドの名前はフィルタ定義からのような私たちは、正確に、はい、それの名前を書き換える方法ではありません、それは実際にここで私たちのカスタムフィルタの呼び出しです内部オーバーライドされたattemptAuthentication
メソッド
私はここで、実際にいくつかの特別な機能が達成するために、サブクラスに遅延させることができる一方、テンプレートパターン、テンプレートモードは、実際に最も一般的な機能は、親によって実装されているされて使用されている説明します。しかし、このプロセスはまだのみ、単一の機能処理を決定する親を呼び出しますが、特定のサブクラスが実装されます。Javaでは、そのような同期キューで有名な抽象的同時実行制御など、このデザインパターンを使用するための多くの場所があるAbstractQueuedSynchronizedは、テンプレートモデルを使用することで、我々は特に微妙な書かれたソースコードに興味を持って缶を見、あります。
まず、フィルタチェーンはに実行します。私は、全体のプロセスを散髪CustomUsernamePasswordAuthenticationFilter
フィルターの親doFilter
方法、およびdoFilter
メソッドをオーバーライド実行されるattemptAuthentication
方法を。
その後、我々は中に入るattemptAuthentication
プロセス、この方法を見て行われています。
まず、取得しbody
たユーザ名とパスワード、およびコンフィギュレーションにUsernamePasswordAuthenticationToken
次の方法にオブジェクト。
ここにフォーカス!
それは、それが誰に渡され、渡さオブジェクトを構築しましたか?
まず、で見てみましょうgetAuthenticationManager()
。この方法はやっています
protected AuthenticationManager getAuthenticationManager() {
return authenticationManager;
}
复制代码
実際の認定マネージャーを返しますが、この証明書の管理、何がありますか?
その後、我々は中に入るAuthenticationManager
唯一のインタフェースで発見された内部、。それはインターフェースなので、それは確かにインスタンス化することはできませんが、ここでは、インスタンス化しAuthenticationManager
たオブジェクトを、真実は一つだけで、春のセキュリティが実装インタフェースというデフォルトのクラスを提供します。はい、実際にはこれがあることであるProviderManager
クラス。
私たちは、最初に彼のソースを見て
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// ~ Static fields/initializers
// =====================================================================================
private static final Log logger = LogFactory.getLog(ProviderManager.class);
// ~ Instance fields
// ================================================================================================
private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
private List<AuthenticationProvider> providers = Collections.emptyList();
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private AuthenticationManager parent;
private boolean eraseCredentialsAfterAuthentication = true;
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, null);
}
public ProviderManager(List<AuthenticationProvider> providers,
AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
}
复制代码
私はいくつかの無関係なコメントやコードがあまりにも格好良いではない、あまりにも長い間だけ有用な部分を残して、削除、またはされて置きます。
まず、私たちはそこにあることがわかったauthenticate
メソッドが、このメソッドの名前は、私たちにはないCustomUsernamePasswordAuthenticationFilter
はい、それはここでは、このメソッドを呼び出すことで、同じ内のコードの最後の行のように。
このメソッドは、主に管理するために使用されProvider
、実際には、ここでは様々な呼ばれているProvider
プロバイダのすべてが実現されていることを確認するために使用される方法AuthenticationProvider
、このインターフェイスを。
私たちは、主であることがわかりprovider.authenticate(authentication)
、彼は本当に方法の検証を使用した、この文。実際の検証プロセスは、私たち自身によって達成、あるいはデフォルトを使用することです。
我々は、使用するアカウントのパスワードを実現をし、Provider
すでに私たちに春のセキュリティによって提供されDaoAuthenticationProvider
、そしてここでは、テンプレートモード(このモードで実際にはかなり一般的なああ)です!
一緒に私たちは何をすべきかを最終的にこのクラスを分析しますか?
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
/**
* The plaintext password used to perform
* PasswordEncoder#matches(CharSequence, String)} on when the user is
* not found to avoid SEC-2056.
*/
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
// ~ Instance fields
// ================================================================================================
private PasswordEncoder passwordEncoder;
/**
* The password used to perform
* {@link PasswordEncoder#matches(CharSequence, String)} on when the user is
* not found to avoid SEC-2056. This is necessary, because some
* {@link PasswordEncoder} implementations will short circuit if the password is not
* in a valid format.
*/
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
public DaoAuthenticationProvider() {
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
// ~ Methods
// ========================================================================================================
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
this.userNotFoundEncodedPassword = null;
}
protected PasswordEncoder getPasswordEncoder() {
return passwordEncoder;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsPasswordService(
UserDetailsPasswordService userDetailsPasswordService) {
this.userDetailsPasswordService = userDetailsPasswordService;
}
}
复制代码
AbstractUserDetailsAuthenticationProvider
このクラスは、本当にあるAuthenticationProvider
抽象クラスインターフェース。
抽象クラスは、親クラスのテンプレートパターンは、サブクラスでありますDaoAuthenticationProvider
抽象クラスは、単に特定のプロセスを定義し、実際の作業は、具体的な実装にサブクラスによって実装を検証することです。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
复制代码
もっと重要なのは、あるretrieveUser
情報システムへのユーザーアクセスをであり、ここでは、アカウントのパスワードで、その後、構築にそれらを置く方法UserDetails
オブジェクト。
校正方法は、あるadditionalAuthenticationChecks
方法は2つのパラメータを受け入れることを1であり、UsernamePasswordAuthenticationToken
他方は、UserDetails
知識を通じて私たちが知っている前に説明するために、UsernamePasswordAuthenticationToken
ユーザーのログインアカウント、パスワード構造により、要求の対象です。UserDetails
アカウントのオブジェクト、システムのパスワード建設があります。限り、我々はこれら2つのオブジェクトが同じ情報で比較すると、我々は、ユーザーがログオンできるかどうかを知ることができます。
呼び出す検証が成功した後にcreateSuccessAuthentication
成功したコールバックを呼び出す方法を、我々は、コールバックでユーザーにトークンを返すことができます。
検証が失敗した場合、また、原理は成功と同じで、失敗したメソッドと呼ばれますが、ユーザーの情報に復帰情報が間違っています。
成功と失敗のコールバックは、我々はまた、カスタマイズする必要があります。成功を介して達成されたAuthenticationSuccessHandler
インターフェース、故障が実装することであるAuthenticationFailureHandler
インターフェイスを。
我々はことを知ってretrieveUser
、それはそれを取得する方法で、この方法は、システム内のユーザー情報を取得しますか?
彼はこの文を実行しますthis.getUserDetailsService().loadUserByUsername(username);
getUserDetailsService()
当社の事業にサービスとこの方法は同じですが、春のセキュリティは、彼らが提供することを実現するために私たちを必要とUserDetailsService
インタフェース1つの方法だけloadUserByUsername
受け入れるusername
パラメータを、私たちは、プロセス内のユーザーに関する情報を取得しています。DAOので、私はデータベースのユーザー名を照会することによってユーザー情報を取得するようになりました。そして、設定されUserDetails
たオブジェクトが返されます。
もちろん、これはUserDetails
またインタフェースである、私たちは、このインタフェース、カスタム実装する必要がUserDetails
使用する実装クラスを。
最後に、私たちはすべてのカスタムコンポーネントがそれを組み合わせるコンフィギュラオブジェクトを渡します。
package com.liangxin.airport.security;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
/**
* 登录校验的配置,就是将各个有关的类进行组合,让spring security可以识别的出来
*
* @author LHD
* @date 2019/12/9 17:17
*/
public class JsonLoginConfigurer<T extends JsonLoginConfigurer<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T, B> {
private CustomUsernamePasswordAuthenticationFilter authFilter;
public JsonLoginConfigurer(){
this.authFilter = new CustomUsernamePasswordAuthenticationFilter();
}
@Override
public void configure(B http){
// 设置filter使用AuthenticationManager
authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// 设置失败的handler
authFilter.setAuthenticationFailureHandler(new JsonLoginFailureHandler());
// 不将认证后的context放入session
authFilter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());
CustomUsernamePasswordAuthenticationFilter filter = postProcess(authFilter);
http.addFilterAfter(filter, LogoutFilter.class);
}
public JsonLoginConfigurer<T, B> loginSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler){
authFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
return this;
}
}
复制代码
概要
インタフェース、継承、デザインパターンの多くで使用されているソースコードので、それのように見えますが、より周りのだろう。
私は、全体のプロセスを要約するためにここにいます:
Filter
- > ProviderManager
- > AbstractUserDetailsAuthenticationProvider
- > DaoAuthenticationProvider
- >AuthenticationSuccessHandler
第一構成されたフィルタは、(後で確認するために)ユーザによって提供されたトークン情報を入力し、ProviderManager
対応する進みProvider
、その後、Provider
前にシステム・トークンを使用して、ユーザ、システムでユーザにユーザ名情報が取得コールに関する情報を比較成功しAuthenticationSuccessHandler
、それがコールに失敗AuthenticationFailureHandler
。
終了
ここでは、同社のプロジェクトので、ソースコードのすべてを直接接続することができない、秘密保持契約がありますが、いつでもメッセージを残すことができる場所あなたはまだわからない場合は、私が答えることを試みる、仕上げ全体のログインプロセスを置きます。