OAuth认证机制入门篇

什么是PAC4J?

pac4j是一个简单而强大的安全引擎,用于Java对用户进行身份验证、获取其配置文件和管理授权,以确保web应用程序安全。它提供了一套完整的概念和组件。它基于Java 8,并在Apache 2许可下使用。它可用于大多数框架/工具和支持大多数认证/授权机制。

已经集成可用的场景

J2E • Spring Web MVC (Spring Boot) • Spring Security (Spring Boot) • Apache Shiro
Play 2.x • Vertx • Spark Java • Ratpack • Undertow
CAS server • JAX-RS • Dropwizard • Apache Knox • Jooby

身份验证机制

OAuth (Facebook, Twitter, Google…) - SAML - CAS - OpenID Connect - HTTP - OpenID - Google App Engine - Kerberos (SPNEGO/Negotiate)
LDAP - SQL - JWT - MongoDB - CouchDB - IP address - REST API

授权机制

Roles/permissions - Anonymous/remember-me/(fully) authenticated - Profile type, attribute
CORS - CSRF - Security headers - IP address, HTTP method

pac4j是一个支持多种支持多种协议的身份认证的Java客户端。

2,pac4j的12种客户端认证机制:目前我只有用过第一和第八种。

  1. OAuth (1.0 & 2.0): Facebook, Twitter, Google, Yahoo, LinkedIn, Github... using the pac4j-oauth module
  2. CAS (1.0, 2.0, SAML, logout & proxy) + REST API support using the pac4j-cas module
  3. HTTP (form, basic auth, IP, header, GET/POST parameter authentications) using the pac4j-http module
  4. OpenID using the pac4j-openid module
  5. SAML (2.0) using the pac4j-saml module
  6. Google App Engine UserService using the pac4j-gae module
  7. OpenID Connect 1.0 using the pac4j-oidc module
  8. JWT using the pac4j-jwt module
  9. LDAP using the pac4j-ldap module
  10. relational DB using the pac4j-sql module
  11. MongoDB using the pac4j-mongo module
  12. Stormpath using the pac4j-stormpath module.

3,maven配置

 1             <dependency>
 2                 <groupId>org.pac4j</groupId>
 3                 <artifactId>spring-security-pac4j</artifactId>
 4                 <version>1.4.3</version>
 5             </dependency>
 6             <dependency>
 7                 <groupId>org.pac4j</groupId>
 8                 <artifactId>pac4j-oauth</artifactId>
 9                 <version>1.8.8</version>
10             </dependency>

View Code

主要是三个jar包:

技术分享

pac4j是一个简单而强大的安全引擎,用于Java对用户进行身份验证、获取其配置文件和管理授权,以确保web应用程序安全。它提供了一套完整的概念和组件。它基于Java 8,并在Apache 2许可下使用。它可用于大多数框架/工具和支持大多数认证/授权机制。

已经集成可用的场景

  • J2E • Spring Web MVC (Spring Boot) • Spring Security (Spring Boot) • Apache Shiro
  • Play 2.x • Vertx • Spark Java • Ratpack • Undertow
  • CAS server • JAX-RS • Dropwizard • Apache Knox • Jooby

身份验证机制

  • OAuth (Facebook, Twitter, Google…) - SAML - CAS - OpenID Connect - HTTP - OpenID - Google App Engine - Kerberos (SPNEGO/Negotiate)
  • LDAP - SQL - JWT - MongoDB - CouchDB - IP address - REST API

授权机制

  • Roles/permissions - Anonymous/remember-me/(fully) authenticated - Profile type, attribute
  • CORS - CSRF - Security headers - IP address, HTTP method

项目地址: https://github.com/pac4j/pac4j/tree/pac4j-3.1.0

----------------------

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.ikane</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>spring-boot-pac4j-demo</name>
<description>Spring-boot PAC4J DEMO</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.pac4j</groupId>
<artifactId>spring-security-pac4j</artifactId>
<version>1.4.1</version>
<exclusions>
<exclusion>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-cas</artifactId>
<version>1.8.5</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

Pac4jConfig.java

package org.ikane;

import org.ikane.security.ClientUserDetailsService;
import org.ikane.service.AccountService;
import org.pac4j.cas.client.CasClient;
import org.pac4j.core.client.Clients;
import org.pac4j.springframework.security.authentication.ClientAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Pac4jConfig {

public static String CAS_LOGIN_URL = "https://casserverpac4j.herokuapp.com/login";

@Value("${oauth.callback.url}")
private String oauthCallbackUrl;

@Autowired
AccountService accountService;

@Bean
CasClient casClient() {
return new CasClient(CAS_LOGIN_URL);
}

@Bean
Clients clients() {
return new Clients(oauthCallbackUrl, casClient());
}

@Bean
ClientUserDetailsService clientUserDetailsService() {
ClientUserDetailsService clientUserDetailsService = new ClientUserDetailsService();
clientUserDetailsService.setAccountService(accountService);
return clientUserDetailsService;
}

@Bean
ClientAuthenticationProvider clientProvider() {
ClientAuthenticationProvider clientAuthenticationProvider = new ClientAuthenticationProvider();
clientAuthenticationProvider.setClients(clients());
clientAuthenticationProvider.setUserDetailsService(clientUserDetailsService());
return clientAuthenticationProvider;
}

}
SecurityConfig.java
package org.ikane;

import org.pac4j.core.client.Clients;
import org.pac4j.springframework.security.authentication.ClientAuthenticationProvider;
import org.pac4j.springframework.security.web.ClientAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;

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

@Autowired
ApplicationContext context;

@Autowired
Clients clients;

@Autowired
ClientAuthenticationProvider clientProvider;

@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers(
"/**/*.css",
"/**/*.png",
"/**/*.gif",
"/**/*.jpg",
"/**/*.ico",
"/**/*.js"
);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.permitAll()
;

http.addFilterBefore(clientFilter(), UsernamePasswordAuthenticationFilter.class);
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);

auth.authenticationProvider(clientProvider);
}

ClientAuthenticationFilter clientFilter() {

String suffixUrl="/*";

ClientAuthenticationFilter clientAuthenticationFilter = new ClientAuthenticationFilter(suffixUrl);
clientAuthenticationFilter.setClients(clients);
clientAuthenticationFilter.setSessionAuthenticationStrategy(sas());
//clientAuthenticationFilter.setAuthenticationManager((AuthenticationManager)clientProvider);

return clientAuthenticationFilter;
/*
return new ClientAuthenticationFilter(
clients: clients,
sessionAuthenticationStrategy: sas(),
authenticationManager: clientProvider as AuthenticationManager
)
*/
}

@Bean
SessionAuthenticationStrategy sas() {
return new SessionFixationProtectionStrategy();
}

}

SpringBootPac4jDemoApplication.java
package org.ikane;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootPac4jDemoApplication {

public static void main(String[] args) {
SpringApplication.run(SpringBootPac4jDemoApplication.class, args);
}
}
ThymeleafConfig.java
package org.ikane;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.extras.springsecurity4.dialect.SpringSecurityDialect;

@Configuration
public class ThymeleafConfig {

@Bean
public SpringSecurityDialect springSecurityDialect() {
return new SpringSecurityDialect();
}
}
IndexController.java
package org.ikane.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
class IndexController {

@RequestMapping("/")
@PreAuthorize("isAuthenticated()")
public String index() {
return "index";
}

}

LoginController.java
package org.ikane.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.pac4j.cas.client.CasClient;
import org.pac4j.core.client.BaseClient;
import org.pac4j.core.client.Clients;
import org.pac4j.core.context.J2EContext;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.exception.RequiresHttpAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {

Logger logger = LoggerFactory.getLogger(LoginController.class);

@Autowired
private Clients clients;

@RequestMapping("/login")
public String login(HttpServletRequest request, HttpServletResponse response, Model model) {

if (isAuthenticated()) {
return "redirect:/";
}
final WebContext context = new J2EContext(request, response);
//定义cas客户端
final CasClient casClient = (CasClient) clients.findClient(CasClient.class);
model.addAttribute("casAuthUrl", getClientLocation(casClient, context));
return "login";
}

//获取客户端的链接
public String getClientLocation(BaseClient client, WebContext context) {

try {
return ((CasClient)client).getRedirectAction(context, false).getLocation();
} catch (RequiresHttpAction e) {
e.printStackTrace();
logger.error("error", e);
return null;
}

//return client.getRedirectAction(context, false, false).getLocation();
}
protected boolean isAuthenticated() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return !(auth instanceof AnonymousAuthenticationToken);
}
}
ClientUserDetails.java
package org.ikane.security;

import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

class ClientUserDetails implements UserDetails {

private static final long serialVersionUID = 6523314653561682296L;

String username;
String providerId;
Collection<GrantedAuthority> authorities;
String password;

public ClientUserDetails() {
// TODO Auto-generated constructor stub
}

public ClientUserDetails(String username, String providerId, Collection<GrantedAuthority> authorities) {
super();
this.username = username;
this.providerId = providerId;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
// TODO Auto-generated method stub
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public String getProviderId() {
return providerId;
}
public void setProviderId(String providerId) {
this.providerId = providerId;
}
public void setUsername(String username) {
this.username = username;
}
public void setAuthorities(Collection<GrantedAuthority> authorities) {
this.authorities = authorities;
}
public void setPassword(String password) {
this.password = password;
}
ClientUserDetailsService.java
package org.ikane.security;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.ikane.service.AccountService;
import org.pac4j.springframework.security.authentication.ClientAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

public class ClientUserDetailsService implements AuthenticationUserDetailsService<ClientAuthenticationToken> {

private AccountService accountService;

public UserDetails loadUserDetails(final ClientAuthenticationToken token) throws UsernameNotFoundException {

Map account = accountService.lookupAccountByProvider(token.getClientName(), token.getUserProfile().getId());

//String username = account.containsKey("displayName") ? account.displayName : ""
String username = "admin";

final List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
for (String role: token.getUserProfile().getRoles()) {
authorities.add(new SimpleGrantedAuthority(role));
}

if (!account.isEmpty() && authorities.isEmpty()) {
// default to user role
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
}

return new ClientUserDetails(username, token.getUserProfile().getId(), authorities);
}

public AccountService getAccountService() {
return accountService;
}

public void setAccountService(AccountService accountService) {
this.accountService = accountService;
}
AccountService.java
package org.ikane.service;

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

import org.springframework.stereotype.Service;

@Service
public class AccountService {
/**
* @Autowired
JdbcTemplate jdbcTemplate
* */

public Map lookupAccountByProvider(String providerName, String providerUserId) {
HashMap<Object,Object> map = new HashMap<>();

/**
*
List results = jdbcTemplate.query(
"select * from account where provider = ? and provider_user_id = ?",
[providerName, providerUserId] as Object[],
new GenericRowMapper()
)

if (results.size() > 1) {
throw new Exception("multiple accounts by provider [${providerName}] for id [${providerUserId}]")
}

* **/

return map;
}

public Boolean createAccountForProvider(String providerName, String providerUserId, String displayName) {

/**
* log.debug("creating new account for displayName=${displayName} using provider=${providerName} with id ${providerUserId}")

int result = jdbcTemplate.update(
"insert into account (display_name, provider, provider_user_id) values (?, ?, ?)",
displayName,
providerName,
providerUserId
)

if (result != 1) {
log.warn("creation of account for provider [${providerName}] and id [${providerUserId}] failed")
return false
}
* */
return true;
}
}
index.html
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spring Pac4j Demo</title>
</head>
<body>
<h2>Index Page sgdsfg</h2>
</body>
</html>123456789
login.html
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorator="layouts/default">
<head>
<title>Login</title>
</head>
<body>
<div id="content" class="sign-in-page" layout:fragment="content">
<h2>Sign In</h2>
<a th:href="${casAuthUrl}" th:class="'oauth-login-link cas-login'">CAS</a>
<a th:href="${gitHubAuthUrl}" th:class="'oauth-login-link github-login'">GitHub</a>
<a th:href="${google2AuthUrl}" th:class="'oauth-login-link google-login'">Google</a>
<a th:href="${twitterAuthUrl}" th:class="'oauth-login-link twitter-login'">Twitter</a>
</div>
</body>
</html>