什么是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基本上基于jdk1.8的环境,由于公司电脑的jdk是1.7,这里就简单的集成CAS就行,其他的认证方式等回家再写,家里的电脑jdk是1.8的。
项目结构
pom .xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="" xmlns:xsi="" xsi:schemaLocation=" "> <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 = ""; @Value("${oauth.callback.url}") private String oauthCallbackUrl; @Autowired AccountService account Service; @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=""> <head> <title>Spring Pac4j Demo</title> </head> <body> <h2>Index Page sgdsfg</h2> </body> </html>
login.html
<!doctype html> <html xmlns:th="" xmlns: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>