Selon le tutoriel Spring Boot et OAuth2
J'ai la structure de projet suivante:
Et le code source suivant:
SocialApplication.class:
@SpringBootApplication
@RestController
@EnableOAuth2Client
@EnableAuthorizationServer
@Order(200)
public class SocialApplication extends WebSecurityConfigurerAdapter {
@Autowired
OAuth2ClientContext oauth2ClientContext;
@RequestMapping({ "/user", "/me" })
public Map<String, String> user(Principal principal) {
Map<String, String> map = new LinkedHashMap<>();
map.put("name", principal.getName());
return map;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.antMatcher("/**").authorizeRequests().antMatchers("/", "/login**", "/webjars/**").permitAll().anyRequest()
.authenticated().and().exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/")).and().logout()
.logoutSuccessUrl("/").permitAll().and().csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
.addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
// @formatter:on
}
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.antMatcher("/me").authorizeRequests().anyRequest().authenticated();
// @formatter:on
}
}
public static void main(String[] args) {
SpringApplication.run(SocialApplication.class, args);
}
@Bean
public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<OAuth2ClientContextFilter>();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
@Bean
@ConfigurationProperties("github")
public ClientResources github() {
return new ClientResources();
}
@Bean
@ConfigurationProperties("facebook")
public ClientResources facebook() {
return new ClientResources();
}
private Filter ssoFilter() {
CompositeFilter filter = new CompositeFilter();
List<Filter> filters = new ArrayList<>();
filters.add(ssoFilter(facebook(), "/login/facebook"));
filters.add(ssoFilter(github(), "/login/github"));
filter.setFilters(filters);
return filter;
}
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
path);
OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
filter.setRestTemplate(template);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(
client.getResource().getUserInfoUri(),
client.getClient().getClientId());
tokenServices.setRestTemplate(template);
filter.setTokenServices(new UserInfoTokenServices(
client.getResource().getUserInfoUri(),
client.getClient().getClientId()));
return filter;
}
}
class ClientResources {
@NestedConfigurationProperty
private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();
@NestedConfigurationProperty
private ResourceServerProperties resource = new ResourceServerProperties();
public AuthorizationCodeResourceDetails getClient() {
return client;
}
public ResourceServerProperties getResource() {
return resource;
}
}
index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=Edge"/>
<title>Demo</title>
<meta name="description" content=""/>
<meta name="viewport" content="width=device-width"/>
<base href="/"/>
<link rel="stylesheet" type="text/css"
href="/webjars/bootstrap/css/bootstrap.min.css"/>
<script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script>
<script type="text/javascript"
src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<h1>Login</h1>
<div class="container unauthenticated">
With Facebook: <a href="/login/facebook">click here</a>
</div>
<div class="container authenticated" style="display: none">
Logged in as: <span id="user"></span>
<div>
<button onClick="logout()" class="btn btn-primary">Logout</button>
</div>
</div>
<script type="text/javascript"
src="/webjars/js-cookie/js.cookie.js"></script>
<script type="text/javascript">
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (settings.type == 'POST' || settings.type == 'PUT'
|| settings.type == 'DELETE') {
if (!(/^http:.*/.test(settings.url) || /^https:.*/
.test(settings.url))) {
// Only send the token to relative URLs i.e. locally.
xhr.setRequestHeader("X-XSRF-TOKEN",
Cookies.get('XSRF-TOKEN'));
}
}
}
});
$.get("/user", function (data) {
$("#user").html(data.userAuthentication.details.name);
$(".unauthenticated").hide();
$(".authenticated").show();
});
var logout = function () {
$.post("/logout", function () {
$("#user").html('');
$(".unauthenticated").show();
$(".authenticated").hide();
});
return true;
}
</script>
</body>
</html>
application.yml:
server:
port: 8080
security:
oauth2:
client:
client-id: acme
client-secret: acmesecret
scope: read,write
auto-approve-scopes: '.*'
facebook:
client:
clientId: 233668646673605
clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
accessTokenUri: https://graph.facebook.com/oauth/access_token
userAuthorizationUri: https://www.facebook.com/dialog/oauth
tokenName: oauth_token
authenticationScheme: query
clientAuthenticationScheme: form
resource:
userInfoUri: https://graph.facebook.com/me
github:
client:
clientId: bd1c0a783ccdd1c9b9e4
clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
accessTokenUri: https://github.com/login/oauth/access_token
userAuthorizationUri: https://github.com/login/oauth/authorize
clientAuthenticationScheme: form
resource:
userInfoUri: https://api.github.com/user
logging:
level:
org.springframework.security: DEBUG
Mais quand j'ouvre le navigateur et que j'essaye de frapper http://localhost:8080
Dans la console du navigateur, je vois:
(index):44 Uncaught TypeError: Cannot read property 'details' of undefined
at Object.success ((index):44)
at j (jquery.js:3073)
at Object.fireWith [as resolveWith] (jquery.js:3185)
at x (jquery.js:8251)
at XMLHttpRequest.<anonymous> (jquery.js:8598)
dans du code:
$.get("/user", function (data) {
$("#user").html(data.userAuthentication.details.name);
$(".unauthenticated").hide();
$(".authenticated").show();
});
Cela se produit parce que /user
réponse avec code d'état 302 et rappel js essayer d'analyser le résultat de localhost:8080
:
Je ne comprends pas pourquoi cette redirection se produit. Pouvez-vous expliquer ce comportement et aider à le corriger?
J'ai pris ce code de https://github.com/spring-guides/tut-spring-boot-oauth2
Il ne se reproduit qu'après le démarrage de l'application client.
Comment reproduire:
Pour tester les nouvelles fonctionnalités, vous pouvez simplement exécuter les deux applications et visiter localhost: 9999/client dans votre navigateur. L'application cliente sera redirigée vers le serveur d'autorisation local, qui donne ensuite à l'utilisateur le choix habituel d'authentification avec Facebook ou Github. Une fois ce contrôle terminé, le client de test retourne, le jeton d'accès local est accordé et l'authentification est terminée (vous devriez voir un message "Bonjour" dans votre navigateur). Si vous êtes déjà authentifié avec Github ou Facebook, vous ne remarquerez peut-être même pas l'authentification à distance
Enfin, j'ai trouvé le problème. Je vois ce comportement dû au fait que les cookies s'affrontent pour le client et le serveur si vous démarrez les deux applications sur localhost.
Cela se produit en raison du fait que la propriété est incorrecte pour le contexte.
Donc, pour corriger l'application, vous devez remplacer:
server:
context-path: /client
avec
server:
servlet:
context-path: /client
J'ai créé un problème sur github:
https://github.com/spring-guides/tut-spring-boot-oauth2/issues/8
et fait la demande de pull:
https://github.com/spring-guides/tut-spring-boot-oauth2/pull/81
Enfin, ma demande de pull a été fusionnée: https://github.com/spring-guides/tut-spring-boot-oauth2/pull/81
Mise à jour: 15 mai 2018
Comme vous avez déjà trouvé la solution, le problème se produit car le JSESSIONID
est écrasé
Mise à jour: 10 mai 2018
Eh bien, votre persévérance avec la 3e prime a finalement porté ses fruits. J'ai commencé à creuser ce qui était différent entre les deux exemples que vous aviez dans le repo
Si vous regardez le repo manual
et /user
mappage
@RequestMapping("/user")
public Principal user(Principal principal) {
return principal;
}
Comme vous pouvez le voir, vous retournez le principal
ici, vous obtenez plus de détails du même objet. Maintenant, dans votre code que vous exécutez à partir de auth-server
dossier
@RequestMapping({ "/user", "/me" })
public Map<String, String> user(Principal principal) {
Map<String, String> map = new LinkedHashMap<>();
map.put("name", principal.getName());
return map;
}
Comme vous pouvez le voir, vous n'avez renvoyé que le name
dans le /user
le mappage et votre logique d'interface utilisateur s'exécute ci-dessous
$.get("/user", function(data) {
$("#user").html(data.userAuthentication.details.name);
$(".unauthenticated").hide();
$(".authenticated").show();
});
La réponse json est donc revenue de /user
api devrait avoir userAuthentication.details.name
par l'interface utilisateur n'a pas ces détails. Maintenant, si j'ai mis à jour la méthode comme ci-dessous dans le même projet
@RequestMapping({"/user", "/me"})
public Map<String, Object> user(Principal principal) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("name", principal.getName());
OAuth2Authentication user = (OAuth2Authentication) principal;
map.put("userAuthentication", new HashMap<String, Object>(){{
put("details", user.getUserAuthentication().getDetails());
}});
return map;
}
Et puis vérifiez l'application, ça marche
Réponse originale
Donc, le problème que vous exécutez un mauvais projet à partir du référentiel. Le projet que vous exécutez est auth-server
qui sert à lancer votre propre serveur oauth
. Le projet que vous devez exécuter se trouve dans le dossier manual
.
Maintenant, si vous regardez le code ci-dessous
OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter(
"/login/facebook");
OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
facebookFilter.setRestTemplate(facebookTemplate);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(),
facebook().getClientId());
tokenServices.setRestTemplate(facebookTemplate);
facebookFilter.setTokenServices(
new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId()));
return facebookFilter;
Et le code réel que vous exécutez a
private Filter ssoFilter(ClientResources client, String path) {
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
path);
OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
filter.setRestTemplate(template);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(
client.getResource().getUserInfoUri(), client.getClient().getClientId());
tokenServices.setRestTemplate(template);
filter.setTokenServices(tokenServices);
return filter;
}
Dans votre courant, le userdetails
du facebook
n'est pas collecté. C'est pourquoi vous voyez une erreur
Parce que lorsque vous vous êtes connecté, l'utilisateur n'a pas collecté ses informations utilisateur. Donc, lorsque vous accédez aux détails, ce n'est pas là. Et donc vous obtenez une erreur
Si vous exécutez le dossier manual
correct, cela fonctionne
Je vois deux requêtes dans votre message.
N -
(index):44 Uncaught TypeError: Cannot read property 'details' of undefined
Cela se produisait parce que vous exécutiez peut-être un mauvais projet (c'est-à-dire un serveur d'authentification) qui a un bogue. Le dépôt contient d'autres projets similaires également sans bogue. Si vous exécutez le projet manuel ou github cette erreur n'apparaîtra pas. Dans ces projets, le code javascript gère correctement les données renvoyées par le serveur après l'authentification.
DEUX -
/user
réponse avec code d'état 302:
Pour comprendre pourquoi cela se produit, voyons la configuration de sécurité de cette application.
Les points finaux "/"
, "/login**"
et "/logout"
sont accessibles à tous. Tous les autres points finaux, y compris "/user"
nécessite une authentification car vous avez utilisé
.anyRequest().authenticated().and().exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/"))
Ainsi, toute demande non authentifiée sera redirigée vers le point d'entrée d'authentification, c'est-à-dire "/"
, demandant à l'utilisateur une authentification. Cela ne dépend pas du démarrage ou non de votre application client. Tant que la demande n'est pas authentifiée, elle sera redirigée vers "/"
. C'est pourquoi le contrôleur de ressort répond avec le statut 302. Une fois que vous vous êtes authentifié avec facebook ou github, les demandes suivantes à "/user"
le point final répondra avec succès avec 200.
ET SUIVANT -
Le point final "/me"
dans votre application est sécurisé en tant que ressource sécurisée avec @EnableResourceServer
. Étant donné que ResourceServerConfiguration
a une priorité plus élevée (ordonnée 3 par défaut) que WebSecurityConfigurerAdapter
(par défaut 100, de toute façon il a déjà ordonné explicitement inférieur à 3 avec l'annotation @Order dans le code), donc ResourceServerConfiguration s'appliquera pour cela point final. Cela signifie que si la demande n'est pas authentifiée, elle ne sera pas redirigée vers le point d'entrée d'authentification, elle retournera plutôt une réponse 401. Une fois que vous êtes authentifié, il répondra avec succès à 200.
J'espère que cela clarifiera toutes vos questions.
PDATE - pour répondre à votre question
Le lien vers le référentiel que vous avez fourni dans votre article contient de nombreux projets. Les projets auth-server, manual et github sont tous similaires (fournissent la même fonctionnalité, c'est-à-dire l'authentification avec facebook et github). Seulement le index.html
in auth-server projet a un bug. Si vous corrigez ce bug qui est remplacer
$("#user").html(data.userAuthentication.details.name);
avec
$("#user").html(data.name);
il fonctionnera également très bien. Les trois projets donneront le même résultat.