General documentation: Article directory Github: github.com/black-ant
A. The preface
Why Pac4j is a authentication tool? Because it really provides a full set of wrapper classes that allow most applications to be integrated quickly without the need for a relational authentication protocol process, users only need to request and acquire users
It should be noted that there are many different versions of Pac4j and their implementations differ greatly. My source code is based on 3.8.0, analyze its ideas, and then separately compare the optimization of subsequent versions, but not much in-depth source details
2. Basic use
One of the features of Pac4j is that it provides a wide range of clients for different vendors. Authentication processing is basically not customized, but here we try to customize our own process
Take OAuth for example:
2.1 Building an Authoriza request
Let’s build a Client to make a request:
OAuth20Client: The most native client invocation class. As you can see below, PAC4J has many custom client classes
public class OAuthService extends BasePac4jService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private final static String CLIENT_ID = "b45e4-41c0-demo";
private final static String CLIENT_SECRET = "0407581-ef15-f773-demo";
private final static String CALLBACK_URL = "http://127.0.0.1:8088/oauth/callback";
/** * Execute Authorization request **@return* /
public void doOAuthRequest(HttpServletRequest request, HttpServletResponse response) {
// Step 1: Build the request Client
OAuth20Configuration config = new OAuth20Configuration();
config.setApi(new DefaultOAuthAPI());
config.setProfileDefinition(new DefaultOAuthDefinition());
config.setScope("user");
config.setKey(CLIENT_ID);
config.setSecret(CLIENT_SECRET);
// Step 2: Build a Client
OAuth20Client client = new OAuth20Client();
// Add the perfect attribute
client.setConfiguration(config);
client.setCallbackUrl(CALLBACK_URL);
// Step 3: Build the request, which is redirected via 302
J2EContext context = new J2EContext(request, response);
client.redirect(context);
// Step 4: Cache data
request.getSession().setAttribute("client", client); }}Copy the code
Note that there is a DefaultOAuthAPI and DefaultOAuthDefinition that defines the SSO path and Profile declaration
DefaultOAuthAPI
DefaultOAuthAPI mainly contains the requested address. DefaultApi20 has two abstract interfaces, and I added one of my own
The DefaultOAuthAPI is unrestricted and allows you to put any interface you want in it for subsequent fetching.
public class DefaultOAuthAPI extends DefaultApi20 {
public String getRootEndpoint(a) {
return "http://127.0.0.1/sso/oauth2.0/";
}
@Override
public String getAccessTokenEndpoint(a) {
return getRootEndpoint() + "accessToken";
}
@Override
protected String getAuthorizationBaseUrl(a) {
return getRootEndpoint() + "authorize"; }}Copy the code
DefaultOAuthDefinition
This declaration acts as a dictionary to translate the data returned by the profile
The entire class does the following:
- Defines the attributes to be returned by the User profile
- Various transformation classes and mappings are defined
- Defines the address of the profile request
- Defines the actual implementation of the transformed data
public class DefaultOAuthDefinition extends OAuth20ProfileDefinition<DefaultOAuhtProfile.OAuth20Configuration> {
public static final String IS_FROM_NEW_LOGIN = "isFromNewLogin";
public static final String AUTHENTICATION_DATE = "authenticationDate";
public static final String AUTHENTICATION_METHOD = "authenticationMethod";
public static final String SUCCESSFUL_AUTHENTICATION_HANDLERS = "successfulAuthenticationHandlers";
public static final String LONG_TERM_AUTHENTICATION_REQUEST_TOKEN_USED = "longTermAuthenticationRequestTokenUsed";
public DefaultOAuthDefinition(a) {
super(x -> new DefaultOAuhtProfile());
primary(IS_FROM_NEW_LOGIN, Converters.BOOLEAN);
primary(AUTHENTICATION_DATE, new DefaultDateConverter());
primary(AUTHENTICATION_METHOD, Converters.STRING);
primary(SUCCESSFUL_AUTHENTICATION_HANDLERS, Converters.STRING);
primary(LONG_TERM_AUTHENTICATION_REQUEST_TOKEN_USED, Converters.BOOLEAN);
}
@Override
public String getProfileUrl(final OAuth2AccessToken accessToken, final OAuth20Configuration configuration) {
return ((DefaultOAuthAPI) configuration.getApi()).getRootEndpoint() + "/profile";
}
@Override
public DefaultOAuhtProfile extractUserProfile(final String body) {
final DefaultOAuhtProfile profile = newProfile();
// The parameter is obtained from the attributes
final String attributesNode = "Get from attributes";
JsonNode json = JsonHelper.getFirstNode(body);
if(json ! =null) {
profile.setId(ProfileHelper.sanitizeIdentifier(profile, JsonHelper.getElement(json, "id")));
json = json.get(attributesNode);
if(json ! =null) {
// The return of CAS is treated differently
if (json instanceof ArrayNode) {
final Iterator<JsonNode> nodes = json.iterator();
while (nodes.hasNext()) {
json = nodes.next();
finalString attribute = json.fieldNames().next(); convertAndAdd(profile, PROFILE_ATTRIBUTE, attribute, JsonHelper.getElement(json, attribute)); }}else if (json instanceof ObjectNode) {
final Iterator<String> keys = json.fieldNames();
while (keys.hasNext()) {
finalString key = keys.next(); convertAndAdd(profile, PROFILE_ATTRIBUTE, key, JsonHelper.getElement(json, key)); }}}else{ raiseProfileExtractionJsonError(body, attributesNode); }}else {
raiseProfileExtractionJsonError(body);
}
returnprofile; }}Copy the code
DefaultDateConverter
This object is used to parse data, such as the time type here
public class DefaultDateConverter extends DateConverter {
public DefaultDateConverter(a) {
super("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
}
@Override
public Date convert(final Object attribute) {
Object a = attribute;
if (a instanceof String) {
String s = (String) a;
int pos = s.lastIndexOf("[");
if (pos > 0) {
s = s.substring(0, pos);
pos = s.lastIndexOf(":");
if (pos > 0) {
s = s.substring(0, pos) + s.substring(pos + 1, s.length()); } a = s; }}return super.convert(a); }}Copy the code
DefaultOAuhtProfile
Can be understood as a TO TO receive data
public class DefaultOAuhtProfile extends OAuth20Profile {
private static final long serialVersionUID = 1347249873352825528L;
public Boolean isFromNewLogin(a) {
return (Boolean) getAttribute(DefaultOAuthDefinition.IS_FROM_NEW_LOGIN);
}
public Date getAuthenticationDate(a) {
return (Date) getAttribute(DefaultOAuthDefinition.AUTHENTICATION_DATE);
}
public String getAuthenticationMethod(a) {
return (String) getAttribute(DefaultOAuthDefinition.AUTHENTICATION_METHOD);
}
public String getSuccessfulAuthenticationHandlers(a) {
return (String) getAttribute(DefaultOAuthDefinition.SUCCESSFUL_AUTHENTICATION_HANDLERS);
}
public Boolean isLongTermAuthenticationRequestTokenUsed(a) {
return(Boolean) getAttribute(DefaultOAuthDefinition.LONG_TERM_AUTHENTICATION_REQUEST_TOKEN_USED); }}Copy the code
2.2 Build a receive object
@GetMapping("callback")
public void callBack(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
logger.info("------> [SSO callback pac4J OAuth logic] <-------");
// Retrieve the cached object from Session
OAuth20Client client = (OAuth20Client) request.getSession().getAttribute("client");
J2EContext context = new J2EContext(request, response);
// Obtain the Credentials corresponding to AccessToken
final Credentials credentials = client.getCredentials(context);
// Obtain a Profile from Profile
final CommonProfile profile = client.getUserProfile(credentials, context);
// The Web returns data information
logger.info("------> Pac4j Demo obtain user information :[{}] <-------", profile.toString());
response.getWriter().println(profile.toString());
}
Copy the code
To sum it up:
- DefaultOAuthAPI: Serves as metadata to identify the requested path
- DefaultOAuthDefinition: Used by the interpreter to interpret the meaning of the return
- DefaultDateConverter: Used to convert data
- DefaultOAuhtProfile: To objects are used to host data
A simple customization that can be adapted to many different OAuth vendors
3. Source list
3.1 OAuth request
3.1.1 Authoriza process
The core class of Authoriza is IndirectClient. Let’s take a brief look at the logic of IndirectClient
The Authoriza authentication is initiated
C01- IndirectClient M101- redirect(WebContext context) ? As you can see earlier, we invoke redirect to initiate the request M102- getRedirectAction - if the request type is ajaxRequest, additional processing is done by ajaxRequestResolver -// M101 pseudocode
public final HttpAction redirect(WebContext context) {
RedirectAction action = this.getRedirectAction(context);
return action.perform(context);
}
// M102 pseudocode
public RedirectAction getRedirectAction(WebContext context) {
this.init();
if (this.ajaxRequestResolver.isAjax(context)) {
RedirectAction action = this.redirectActionBuilder.redirect(context);
this.cleanRequestedUrl(context);
return this.ajaxRequestResolver.buildAjaxResponse(action.getLocation(), context);
} else {
String attemptedAuth = (String)context.getSessionStore().get(context, this.getName() + "$attemptedAuthentication");
if (CommonHelper.isNotBlank(attemptedAuth)) {
this.cleanAttemptedAuthentication(context);
this.cleanRequestedUrl(context);
throw HttpAction.unauthorized(context);
} else {
return this.redirectActionBuilder.redirect(context); }}} our fleet - RedirectActionBuilder (OAuth20RedirectActionBuilder) M201 - redirect - generate state and into the session2- this. The configuration. BuildService: build a OAuth20Service3- Obtain an authorizationUrl using the param attribute. -RedirectAction. redirect(authorizationUrl) : initiates authentication// M201 pseudocode
public RedirectAction redirect(WebContext context) {
// This is pseudo code that generates state from generateState and puts it into session
String state=this.configuration.isWithState()? generateState :null;
// m201-2: OAuth20Service is the OAuth business class
OAuth20Service service = (OAuth20Service)this.configuration.buildService(context, this.client, state);
// m201-3: set the authentication address
String authorizationUrl = service.getAuthorizationUrl(this.configuration.getCustomParams());
return RedirectAction.redirect(authorizationUrl);
}
// We haven't seen the actual request yet, so let's look at the bottomLet's go back to the M101 method perform c-redirectAction m-perform (WebContext context) -this.type == RedirectAction.RedirectType.REDIRECT ? HttpAction.redirect(context, this.location) : HttpAction.ok(context, this.content);
// The truth comes out
public static HttpAction redirect(WebContext context, String url) {
context.setResponseHeader("Location", url);
context.setResponseStatus(302);
return new HttpAction(302); } He uses302The redirection status code, which is completed by the browser, where the charging relation address is HTTP:/ / 127.0.0.1 / sso/oauth2.0 / the authorize? The payload = code&client _id = b7a8cc2a a78 & redirect_uri - 5 dec - 4 = HTTP % % 2 f % 2 f127. 3 a 0.0.1%3 a9081%2 fmfa - client fcallba % 2 foauth % 2 ck%3Fclient_name%3DCasOAuthWrapperClient
Copy the code
Addendum 1: OAuth20Service
OAuth20Service is an OAuth business class that contains common OAuth operations
3.1.2 AccessToken process
In the previous article, we built a CallBack method for OAuth requests that will be called back after SSO authentication is complete. Let’s take a look at some of the interesting points:
public void oauthCallBack(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
// This can be compared to building Context
// This creates a loop that sends the state back
CasOAuthWrapperClient client = (CasOAuthWrapperClient) request.getSession().getAttribute("oauthClient");
// Step 2: Get the AccessToken
J2EContext context = new J2EContext(request, response);
final OAuth20Credentials credentials = client.getCredentials(context);
final CommonProfile profile = client.getUserProfile(credentials, context);
response.getWriter().println(profile.toString());
}
Copy the code
So let’s see what the getCredentials method does
C01- IndirectClient
M103- getCredentials(WebContext context)
- thisInit () : request this is a main assertion - CommonHelper. AssertNotBlank ("key".this.key);
- CommonHelper.assertNotBlank("secret".this.secret);
- CommonHelper.assertNotNull("api".this.api);
- CommonHelper.assertNotNull("hasBeenCancelledFactory".this.hasBeenCancelledFactory);
- CommonHelper.assertNotNull("profileDefinition".this.profileDefinition);
public final C getCredentials(WebContext context) {
this.init();
C credentials = this.retrieveCredentials(context);
if (credentials == null) {
context.getSessionStore().set(context, this.getName() + "$attemptedAuthentication"."true");
} else {
this.cleanAttemptedAuthentication(context);
}
return credentials;
}
// Continue the index and see more complex
C03- BaseClient
M301- retrieveCredentials
- this. CredentialsExtractor. Extract (context) : obtain a credentials object? - This object is the Code object previously returned by Authoriza after completion :PS001 -this.authenticator.validate(credentials, context) : Initiate validation? - This is OAuth20Authenticator, see more/ / added PS001
#OAuth20Credentials# | code: OC-1-wVu2cc3p33ChsQshKd1rUabk6lggPB1QhWh | accessToken: null| C04 - OAuth20Authenticator M401 - retrieveAccessToken - from OAuth20Credentials code - by OAuth20Configuration build OAuth20Service, call getAccessToken// M401 pseudo-code: this is very clear
protected void retrieveAccessToken(WebContext context, OAuthCredentials credentials) {
OAuth20Credentials oAuth20Credentials = (OAuth20Credentials)credentials;
String code = oAuth20Credentials.getCode();
this.logger.debug("code: {}", code);
OAuth2AccessToken accessToken;
try {
accessToken = ((OAuth20Service)((OAuth20Configuration)this.configuration).buildService(context, this.client, (String)null)).getAccessToken(code);
} catch (InterruptedException | ExecutionException | IOException var7) {
throw new HttpCommunicationException("Error getting token:" + var7.getMessage());
}
this.logger.debug("accessToken: {}", accessToken);
oAuth20Credentials.setAccessToken(accessToken);
}
C05- OAuth20Service
M501- getAccessToken
- OAuthRequest request = this.createAccessTokenRequest(code, pkceCodeVerifier);
- this.sendAccessTokenRequestSync(request);
M502- sendAccessTokenRequestSync
- (OAuth2AccessToken)this.api.getAccessTokenExtractor().extract(this.execute(request)); ? Httpclient.execute (userAgent, request.getheaders (), request.getVerb(), httpClient.execute(userAgent, request.getheaders (), request. request.getCompleteUrl(),request.getByteArrayPayload()); ? - PS002// PS002 Supplementary: See the following figure for parameters
http:/ / 127.0.0.1 / sso/oauth2.0 / accessToken?
Copy the code
3.1.3 the UserInfo
How to exchange Userinfo for AccessToken
// Step 1: The starting point
final CommonProfile profile = client.getUserProfile(credentials, context);
C03- BaseClient
M302- getUserProfile
- U profile = retrieveUserProfile(credentials, context);
M303- retrieveUserProfile
- this.profileCreator.create(credentials, context); ? - OAuth20ProfileCreator: M601 C06- OAuthProfileCreator M601- create-t token =this.getAccesstoken (Credentials) : The Token - is obtainedreturn this.retrieveUserProfileFromToken(context, token);
M602- retrieveUserProfileFromToken
- finalOAuthProfileDefinition<U, T, O> profileDefinition = configuration.getProfileDefinition(); ? - OAuthProfileDefinition Builds the request, including the type to send -finalString profileUrl = profileDefinition.getProfileUrl(accessToken, configuration); ? - Profile Address -final S service = this.configuration.buildService(context, client, null); ? - Build a Service -finalString body = sendRequestForData(service, accessToken, profileUrl, profileDefinition.getProfileVerb()); ? - Request Profile, where the data is actually called -finalU profile = (U) configuration.getProfileDefinition().extractUserProfile(body); ? - Parse into a Profile object - addAccessTokenToProfile(Profile, accessToken); ? - Build the final objectCopy the code
3.2 the SAML article
3.2.1 Initiating a Request
// Step 1: Initiate a request- Build a Configuration - build a Client - because the SAML API is in metadata, So there is no need to inject API --> Invoke RedirectAction action = client.getreDirectAction (context); action.perform(context); -returnredirectActionBuilder.redirect(context); ? - the same routine, the builder here is SAML2RedirectActionBuilder// Finally build a SAML 302 request as usual
Copy the code
Look at the result of the request
3.2.2 Receiving data
The latter is still exactly the same, except that the Authenticator is now SAML2Authenticator
final SAML2Client client = (SAML2Client) request.getSession().getAttribute("samlclient");
// Get the J2EContext object
J2EContext context=new J2EContext(request,response);
final SAML2Credentials credentials = client.getCredentials(context);
// Obtain profile data
final CommonProfile profile = client.getUserProfile(credentials, context);
response.getWriter().println(profile.toString());
C- SAML2Authenticator
M- validate
- finalSAML2Profile profile = getProfileDefinition().newProfile(); ? - obtain the return Profile - Profile. AddAuthenticationAttribute (SESSION_INDEX, credentials. GetSessionIndex ()); - profile.addAuthenticationAttribute(SAML_NAME_ID_FORMAT, nameId.getFormat()); - profile.addAuthenticationAttribute(SAML_NAME_ID_NAME_QUALIFIER, nameId.getNameQualifier()); - profile.addAuthenticationAttribute(SAML_NAME_ID_SP_NAME_QUALIFIER, nameId.getSpNameQualifier()); - profile.addAuthenticationAttribute(SAML_NAME_ID_SP_PROVIDED_ID, nameId.getSpProviderId()); ? - Configure related properties// final CommonProfile profile = client.getUserProfile(credentials, context);
Copy the code
4. In-depth analysis
Pac4j is a good open source project. In terms of process, it has very good extensibility (PS: personal writing products like extensibility, everything can be matched), which is a big advantage in open source. Its overall process can be basically viewed as the following parts
The Configuration system
The Client system
Credentials system
Profile system
In the case of so many systems, the overall container coordination is done by Context, and the unified request redirection is done by RedirectAction.
Why specifically mention RedirectAction?
In my opinion, PAC4J abstracts all requests into two parts, one is to initiate authentication and the other is to return callback. With these two intuitive operations as the boundary, and then adding operations such as obtaining authentication information, the user basically does not see the call to the request.
SequenceDiagram Demo->>Demo ->>Demo ->>Demo ->>Demo ->>Demo client Demo->>SSO ->>Demo: Return authentication information Demo->>Demo: Definition parses and returns, obtaining a AccessToken Credentials Demo->>SSO: Obtains the authentication information through token SSO->>Demo: Return user information Demo->>Demo: resolves to Profile
5. Open source analysis
So what benefits can be learned from PAC4J?
First, what is the positioning of PAC4J?
Pac4j is an authentication tool, or SDK, that solves the complexity of the authentication process and allows users to access user information directly with a simple call.
The first advantage is compatibility and configurability. I provide so many clients that you can adjust them directly or customize them yourself. It doesn’t matter.
The second advantage of PAC4J in terms of code structure is clarity.
We can feel from the above analysis, what can be done, how to do, how to name, in fact, the specification is good, simple implementation can meet the requirements.
And the third advantage, I think, is low coupling.
The way THAT Pac4J aggregates Maven, you can just reference the relevant package to implement the protocol. There is also a low code-to-code dependency, which is also a great benefit for customization and worth learning from.
6. Summary
The pac4J tool is an ideal tool if you want to get it online quickly,
Personal when writing demo, also often use him to do basic test, don’t say, really good use
The code has been posted on Git (case 4.6.2) and can be viewed directly.