4. Seguretat
1. HTTPS
Avui en dia, necessitem afegir tot el que puguem per assegurar les nostres aplicacions. Estudiarem el concepte de tokens per autoritzar i autenticar les nostres sol·licituds, però necessitem una capa extra, https.
En aquesta pàgina web https://tiptopsecurity.com/how-does-https-work-rsa-encryption-explained/ podeu trobar com funciona https.
1.1. Certificat
Primerament, necessitem generar certificats, o comprar-los. Utilitzarem l'eina keytool inclosa amb el kit de desenvolupament de Java per generar-los. Aquesta comanda genera un parell de certificats (públic i privat).
| Bash | |
|---|---|
Després d'executar aquesta comanda, hem de respondre sobre qui som, de la següent manera:
1.2. Configurar Spring
Un cop finalitzat el procés, hem d'afegir el certificat dins del nostre projecte. Per exemple, dins de /resources/keystore. Finalment, hem de carregar el certificat i habilitar SSL, simplement afegint aquestes línies a application.properties:
I això és tot, quan Spring comenci veurem que està funcionant amb el protocol https:
| Bash | |
|---|---|
i en la sol·licitud probablement els navegadors no confiaran en el nostre certificat (haurem d'afegir una excepció de confiança):
2. Spring Security
Spring Security és un projecte paraigua que agrupa tots els mecanismes referents a la seguretat. Necessitem afegir:
| XML | |
|---|---|
i automàgicament:
- La configuració per defecte està habilitada, a través d'un filtre, anomenat
SpringSecurityFilterChain. - Es crea un bean de tipus
UserDetailsServiceamb un usuari anomenatuseri una contrasenya aleatòria que es mostra per la consola. - El filtre es registra en el contenidor de servlets per a totes les sol·licituds.
Encara que no has configurat gaire, té moltes conseqüències:
- Requereix autenticació per interactuar amb la nostra aplicació
- Genera un formulari de login per defecte.
- Genera un mecanisme de logout
- Protegeix l'emmagatzematge de contrasenyes amb
BCrypt. - Proporciona protecció contra atacs CSRF, Fixació de Sessió, Clickjacking...
Podem crear una classe de configuració, amb aquest contingut:
Atenció
Tornarem a aquesta classe per afegir més configuracions. El més interessant és el mètode configure i la creació de diversos Beans utilitzats per altres classes.
2.1. Dades d'Usuaris per a Autenticació i Autorització (Models)
Important
A partir d'ara, aquest exemple es basa en una pàgina web famosa https://www.bezkoder.com. Expliquem tot el necessari de l'exemple que podeu veure aquí.
En aquesta secció prepararem la nostra aplicació per identificar usuaris amb diversos rols. Amb aquests rols, podrem concedir o no accés a diversos recursos.
2.2. Usuaris & Rols
Per fer això, necessitem crear una classe User, per emmagatzemar aquesta informació a la nostra base de dades. Aquest usuari podria tenir una col·lecció de rols. Podem fer-ho amb una relació de molts a molts.
basat en aquesta enumeració, farem una classe Role per emmagatzemar els rols que la nostra aplicació suportarà:
| Java | |
|---|---|
finalment, una classe User com la següent. Afegim noves anotacions de validació:
2.3. Repositori d'Usuaris i Rols
Necessitem crear els nostres repositoris per a les últimes entitats, creant interfícies com normalment fem.
També afegim mètodes per comprovar l'existència de l'usuari per nom i correu electrònic, i mètodes per trobar per nom, tant User com Role.
2.4. UserDetails
Spring necessita que algú implementi la interfície UserDetails, molt important perquè Spring Security utilitzarà un UserDetails. UserDetails conté la informació necessària per construir un objecte Authentication a partir de DAOs o altres fonts de dades de seguretat. Creem una classe, anomenada UserDetailsImpl, que:
- Ha de tenir els camps
usernameipassword, i els getters. Heu de respectar els noms, qualsevol canvi està prohibit. Aquests mètodes seran utilitzats per les classes d'autenticació. - Sobreescriure mètodes de
UserDetails, per exemplegetAuthorities()i diversos mètodes per controlar si l'usuari està bloquejat, caducat, etc.
Classe completa:
Tingues en compte que:
- Utilitzem
private Collection<? extends GrantedAuthority> authorities;per emmagatzemar les autoritats (és a dir, rols) en un format que és entès per Spring Security. - En lloc de crear un constructor, creem un
builder(), que rep unUser, extreu la informació d'aquest i transforma laList<Role>en autoritats, i després, el builder crida al constructor.
En lloc de crear un User i Role Service, crearem un UserDetailSeriveImpl (que implementa UserDetailService de Spring), per tal de recuperar un User del repositori, i després retorna un UserDetailImpl, de la següent manera:
2.5. Càrregues de DTO's
En aquesta secció veurem les classes necessàries que emmagatzemen informació per a:
- Registrar un nou usuari, per rebre informació del client i emmagatzemar un nou usuari. Aquesta classe és
SignupRequest. - Iniciar sessió d'un usuari, per accedir al nostre sistema. Aquesta classe és
LoginRequest. JwtResponse, relacionada com a resposta a la sol·licitud d'inici de sessió. Aquesta resposta contindrà un Token JWT, utilitzat per autoritzar sol·licituds posteriors. Aquesta classe ésJwtResponse.
2.6. SignupRequest
Aquesta classe conté informació per registrar un nou usuari. Conté anotacions de validació.
| Java | |
|---|---|
| JSON | |
|---|---|
2.7. LoginRequest
Molt senzilla:
| Java | |
|---|---|
i l'objecte json associat serà:
Atenció
En aquesta classe podem marcar els camps com a obligatoris, amb l'anotació @NotBlank de javax.validation.constraints.NotBlank. Has d'afegir:
2.8. JwtResponse
Aquesta classe DTO és la classe que es retorna com a sol·licitud de login. Ha de contenir poca informació sobre el nostre usuari i el més important, un token JWT. Aquest token s'utilitzarà per autoritzar-nos, com estudiarem a la següent secció.
| Java | |
|---|---|
Tingues en compte que:
- Els camps d'aquesta classe es poblaran a partir de la classe
User, com un DTO. - Hem canviat el format del rol, de la classe
RoleaString, amb una millor gestió en els clients. - El
String tokenés on s'emmagatzema el token JWT. De fet, un token és una cadena de text, com mostrarem ara.
3. Tokens JWT
3.1. Què és un token?
JSON Web Tokens (JWT) s'han introduït com un mètode de securitzar la comunicació segura entre dues parts. Es va introduir amb l'especificació RFC 7519 per l'Internet Engineering Task Force (IETF).
Tot i que podem utilitzar JWT amb qualsevol tipus de mètode de comunicació, avui en dia, JWT és molt popular per gestionar l'autenticació i l'autorització sobre HTTP.
Primer, necessitaràs conèixer algunes característiques de HTTP:
- HTTP és un protocol sense estat, el que significa que una sol·licitud HTTP no manté l'estat. El servidor no és conscient de cap sol·licitud anterior enviada pel mateix client.
- Les sol·licituds HTTP haurien de ser autònomes. Han d'incloure informació sobre sol·licituds anteriors que l'usuari ha fet en la mateixa sol·licitud.
Hi ha algunes maneres de fer això, però la manera més popular és establir un session_id, que és una referència a la informació de l'usuari:
- El servidor emmagatzemarà aquest ID de sessió en memòria o en una base de dades. El client enviarà cada sol·licitud amb aquest ID de sessió.
- El servidor pot obtenir informació sobre el client utilitzant aquesta referència.
- Normalment, aquest ID de sessió s'envia a l'usuari com una cookie.
Aquí tens el diagrama de com funciona l'autenticació basada en sessions.
D'altra banda, amb JWT, quan el client envia una sol·licitud d'autenticació al servidor, aquest enviarà un token JSON al client, que inclou tota la informació sobre l'usuari juntament amb la resposta.
El client enviarà aquest token amb totes les sol·licituds posteriors. Per tant, el servidor no haurà d'emmagatzemar cap informació sobre la sessió. Però hi ha un problema amb aquest enfocament. Qualsevol pot enviar una sol·licitud falsa amb un token JSON fals i fer-se passar per algú que no és.
Per exemple, suposem que després de l'autenticació, el servidor retorna un objecte JSON al client amb el nom d'usuari i el temps d'expiració. Així, com que l'objecte JSON és llegible, qualsevol pot editar aquesta informació i enviar una sol·licitud amb ella. El problema és que no hi ha manera de validar aquesta sol·licitud.
Aquí és on entra en joc la signatura testimoni. Així que en lloc d'enviar només un token JSON normal, el servidor enviarà un token signat, que pot verificar que la informació no ha canviat.
3.2. Estructura d'un JWT
Parlem de l'estructura d'un JWT a través d'un token d'exemple:
| JSON | |
|---|---|
Com podeu veure, hi ha tres seccions en aquest JWT, cadascuna separada per un punt.
Nota
La codificació Base64 és una manera d'assegurar que les dades no es corrompin, ja que no comprimeixen ni xifren les dades, sinó que simplement les codifiquen d'una manera que la majoria dels sistemes poden entendre. Podeu llegir qualsevol text codificat en Base64 simplement descodificant-lo.
La primera secció del JWT és un header, que és una cadena codificada en Base64. Si descodifiqueu el header, es veuria així:
La secció del header conté l'algoritme de hash, que es va utilitzar per generar la signatura del token i el tipus.
La segona secció és el payload que conté l'objecte JSON que es va enviar de tornada a l'usuari. Com que només està codificat en Base64, qualsevol pot descodificar-lo fàcilment. És obligatori no incloure dades sensibles en els JWT, com ara contrasenyes o informació personal identificable.
Normalment, el cos del JWT es veurà així, tot i que no necessàriament s'aplica:
Nota
La majoria de les vegades, la propietat sub contindrà l'ID de l'usuari, la propietat iat (issued at), abreujada com emès a, és el segell de temps d'emissió del token. També podeu veure algunes propietats comunes, com ara eat o exp, que és el temps d'expiració del token.
Totes aquestes propietats són els claims del token, la informació.
La secció final és la signatura del token. Aquesta es genera fent un hash de la cadena creada amb les dues seccions anteriors i una contrasenya secreta, utilitzant l'algoritme esmentat a la secció del header.
Podeu visitar https://www.javainuse.com/jwtgenerator i https://jwt.io per generar tokens i provar amb diverses dades, secrets i hashes.
3.3. JWT Library class
Afegirem al pom.xml les següents dependències:
Aquesta classe és la que genera i comprova la integritat dels tokens. Anem a mostrar aquesta classe i quins elements necessita. Aquesta classe serà una classe de biblioteca amb mètodes per crear tokens, validar-los i extreure informació d'ells. Podem trobar aquesta classe amb noms com JWTUtils o JWTTokenProvider. L'esquelet d'aquesta classe és el següent:
- Càrrega o definició de constants del token
- Mètode per generar JWT a partir d'un objecte
Authentication - Mètodes per obtenir informació del token
- Mètode per validar el token (signatura)
| Java | |
|---|---|
3.4. Generar Tokens
Vegem cada mètode. Primer, necessitem un objecte Authentication. Hem de saber que aquest objecte representa un altre token (no el nostre JWT) amb les credencials de l'objecte que volem identificar.
Veiem que:
- Rebem un objecte
Authentication, en el qual hem guardat unUserDetailsImpl. Com totes les implementacions de detalls d'usuari, podem obtenir el nom d'usuari, i l'utilitzem per establir el subjecte del token. - Establim
IATal moment actual. - Establim el temps d'expiració.
- Establim l'algoritme de xifrat i la paraula secreta, i el token està llest per...
- El mètode
compact()crea i transforma el token a String.
Nota
El jwtSecret és la contrasenya que necessitem. És una bona idea emmagatzemar-la dins de application.properties i recuperar-la a una variable, com jwtExpiration. Podem utilitzar l'anotació @Value:
| Java | |
|---|---|
3.5. Validació de Tokens
Molt fàcil:
En aquest mètode comprovem si és un token vàlid. Utilitzem parseClaimsJws(String token), després d'assignar la contrasenya secreta per obtenir els claims. Si hi ha algun problema amb la integritat del token, podria aparèixer una excepció. Obtenim els claims dins d'un bloc try-catch i informem si passa alguna cosa. Retornarem true si no es captura cap excepció.
Nota
Recorda que els claims són el contingut del payload del token. No necessitem els claims en aquest mètode, només comprovar si tot està bé.
4. Authentication Controller
Ara que també sabem com crear tokens i l'estructura de User a la nostra base de dades, és hora d'exposar el nostre camí per registrar i iniciar sessió d'usuaris. Per tant, només són obligatoris dos mètodes. La classe podria ser alguna cosa així:
| Java | |
|---|---|
- L'anotació
@CrossOriginpermet sol·licituds de cross-origin en classes de controlador específiques i/o mètodes de controlador. Es processa si es configura unHandlerMappingapropiat. El Cross-Origin Resource Sharing (CORS) és un concepte de seguretat que permet restringir els recursos implementats en navegadors web. Evita que el codi JavaScript produeixi o consumeixi sol·licituds contra un origen diferent. Per exemple, la vostra aplicació web s'està executant al port 8080 i mitjançant JavaScript esteu intentant consumir serveis web RESTful des del port 9090. En aquestes situacions, us trobareu amb el problema de seguretat de Cross-Origin Resource Sharing als vostres navegadors web. @RequestMapping("/api/auth")indica que tots els controladors estan dins del camí/api/auth.
Vegem-los, però abans d'estudiar els mètodes, aquesta classe té aquestes variables necessàries:
@Autowired AuthenticationManager authenticationManager;→ s'utilitza per crear un token d'Authentication, utilitzat pel context de seguretat de Spring i pel generador de tokens.@Autowired UserRepository userRepository;→ s'utilitza per accedir i desar usuaris.@Autowired RoleRepository roleRepository;→ per comprovar si els rols que arriben a la sol·licitud són vàlids.@Autowired PasswordEncoder encoder;→ s'utilitza per encriptar la contrasenya de l'usuari.@Autowired JwtUtils jwtUtils;→ s'utilitza per crear tokens JWT.
Ara que hem presentat els actors, anem a la funció.
4.1. Signup (nou usuari)
El mètode encarregat de crear nous usuaris rebrà un SignupRequest amb aquestes dades:
| JSON | |
|---|---|
el cos del mètode és el següent, i vegem-ho per blocs:
En aquesta primera part:
- Comprovem si existeix algun usuari amb el mateix nom d'usuari o correu electrònic, consultant el nostre repositori. Si apareix algun error, retornarem un
ResponseEntitycom a bad request amb un missatge descriptiu. - Finalment, creem un nou usuari amb nom d'usuari, correu electrònic i una contrasenya encriptada.
El següent bloc és responsable d'obtenir els rols (emmagatzemats en un JSONArray de cadenes) i transformar-los en un Set<Role>.
Vegem-ho:
- Primer comprovem si el conjunt de rols està buit. Si és cert, establim un nou
RoleambERole.ROLE_USERper defecte. - En cas contrari, hem de recórrer tots els rols que obtenim, comprovant cada rol a la base de dades i creant l'objecte
Rolecorresponent.
Finalment, assignem el Set<Role> a l'usuari creat en el primer bloc, i en el tercer bloc només necessitem emmagatzemar el nou usuari amb UserRepository i enviar una resposta d'ok al client.
| Java | |
|---|---|
4.2. Signin (Validar-se o accedir)
Nota
En valencià:
- Signin: registrarse, accedir al login.
- Signup: inscribirse, donar-se d'alta. La primera vegada
Aquest controlador s'utilitza per iniciar sessió d'un usuari a la nostra aplicació. Com hem estudiat, el DTO que rebem en el HTTP_POST és la classe LoginRequest, com aquesta:
El mètode encarregat serà:
Tingues en compte:
- L'anotació
@Validcomprova que l'objecte JSON coincideixi amb la classeLoginRequest(mira les anotacions d'aquesta classe). - Creem un token d'
Authentication(aquest no és el token JWT !!!) amb el nom d'usuari i la contrasenya rebuts. La contrasenya encara no està encriptada. - L'últim token d'
Authenticationes configura alSecurityContextHolder, un objecte que conté un mínim de seguretat a nivell de fil. En aquest pas és quan el subsistema de seguretat de Spring funciona, demanant aUserDetailServiceun usuari amb aquest nom d'usuari i contrasenya a la nostra base de dades, i es guarda en un BeanUserDetailsa la memòria. Si alguna credencial és incorrecta, es llançarà una excepció, que serà gestionada pel nostre sistema (ho estudiarem més endavant). - Amb aquest token (autenticació) generem el nostre token. Recorda que dins de
jwtUtils, només obtenim el nom d'usuari per generar el subjecte del nostre token. - Obtenim l'objecte
UserDetailsamb el mètodegetPrincipal(), i - Transformem la llista d'autoritats en una llista de cadenes (has estudiat com mapar llistes? I filtrar? I reduir?).
- Finalment, creem i retornem un
ResponseEntity, amb l'http_Status ok, amb unJwtResponsedins. Token més atributs. Un exemple deJWTresponeés:
Atenció
L'últim token té salts de línia addicionals per evitar sortir del marge del paper, és una cadena completa.
Si el nom d'usuari no es troba a la base de dades, la nostra aplicació crea la següent resposta:
| JSON | |
|---|---|
5. Tokens en funcionament
Ara que el registre d'usuaris i l'inici de sessió estan implementats, fem una pregunta, Què fa l'aplicació client amb el JWTResponse que ha rebut després del procés d'inici de sessió? Les dades de l'usuari normalment s'utilitzen per a la interfície (nom complet, avatar, etc.). Però què passa amb els tokens?
La resposta, com estudiarem més endavant, és emmagatzemar-lo, i després enviar-lo al servidor en cada sol·licitud com a mètode d'Autenticació i Autorització.
5.1. Enviant tokens
Per enviar un token, necessitem adjuntar-lo a la secció Header, creant un paràmetre Authorization amb el valor Bearer token_recieved, com podeu veure en aquesta captura de pantalla de Postman:
Important
Recorda: la paraula Bearer més un espai en blanc més tot el token rebut (com a String)
D'acord, així doncs, l'aplicació client ens enviarà el token a través de la sol·licitud, però, com fa el nostre servidor per obtenir i comprovar el token? La resposta és que hem de dir-ho en forma de filtre.
6. Configuració de Seguretat
La classe que configura la seguretat és WebSecurityConfig. Anem a explicar-la per blocs de nou. Aquesta classe està composta per un conjunt de Beans que s'utilitzaran en tot el projecte (recorda la injecció de codi). Explicarem només el necessari.
| Java | |
|---|---|
Aquesta anotació @Configuration indica a Spring que ha de carregar aquesta classe. També permet que Spring utilitzi anotacions de filtres pre i post (ho estudiarem més endavant).
| Java | |
|---|---|
Aquests beans s'explicaran més endavant. En poques paraules, són responsables de gestionar errors com a manejador d'excepcions i de com aplicar cadenes de filtres per autenticar usuaris.
| Java | |
|---|---|
Aquest Bean utilitza el UserDetailService i el PasswordEncoder per fer el procés d'autenticació. Això significa accedir a la base de dades i comprovar l'usuari i la contrasenya (amb el mateix encoder que utilitzem per emmagatzemar usuaris). Els següents Beans creen l'AuthenticationManager i l'encoder.
| Java | |
|---|---|
I finalment, una de les configuracions més importants (i difícils d'entendre), la cadena de filtres de seguretat. Les cadenes de filtres són codi que posem al mig entre el client i el servidor. Aquests filtres intercepten la sol·licitud, l'analitzen i després, depenent del resultat del filtre, simplement passen el control al servidor o envien una resposta al client. Aquest filtre podria canviar la sol·licitud, afegint o eliminant informació que serà utilitzada pel servidor.
Tingueu en compte que:
- Habilitem CORS (Cross-Origin Requests) i deshabilitem CSRF (Cross Site Request Forgery)
- Establim l'authenticationEntryPoint
- Permetem l'accés a tots els camins a
/api/auth/** - Permetem l'accés a tots els camins a
/api/test/** - Qualsevol altra sol·licitud necessitarà ser autenticada
Finalment, afegim el filtre abans i el proveïdor d'autenticació.
En una visió relaxada, per entendre-ho, els filtres es combinen amb anotacions que indiquen on i quan hem de comprovar l'Autenticació i l'Autorització.
6.1. AuthTokenFilter
Aquesta classe conté el procés del token rebut dels clients. Ha d'implementar OncePerRequestFilter i sobreescriure doFilterInternal().
En paraules nostres, aquest mètode s'executarà quan es faci una sol·licitud i:
- Extreu el token de la sol·licitud, retornant només la informació rellevant (elimina
Bearer). - Comprova que el token sigui vàlid, i:
- Extreu el nom d'usuari (està en el payload del token) i
- Obté aquest
UserDetailsi crea unUsernamePasswordAuthenticationTokenper ser injectat a la resta de la sol·licitud-resposta, òbviament amb autoritats. - Finalment, continua amb el següent filtre, si existeix, cridant
filterChain.doFilter(req,res). Si no hi ha cap filtre, el dispatcher passa el control al controlador sol·licitat.
6.2. AuthEntryPoint
Aquesta classe conté codi que s'executarà quan aparegui una excepció. Aleshores, crea un cos genèric dins de la resposta i estableix una resposta precisa per al client.
7. Controller Authorization
Només ens queda dir quina sol·licitud necessita ser autoritzada. Recorda que amb altres classes preparem l'autenticació, Qui ets?. Ara necessitem preguntar Què pots fer?
Com marquem en la nostra filterChain, afegim addFilterBefore per comprovar els rols. Però, on hem de dir els rols que capturen cada sol·licitud? La resposta és fàcil: els controladors. Vegem un controlador de prova amb filtre d'autorització:
- No apareix cap anotació: tots els rols poden fer aquesta sol·licitud
@PreAuthorize("hasRole('role')")→ Aquesta anotació indica el rol que està autoritzat per fer aquesta sol·licitud. Pots combinar més d'un rol ambor
8. Exercici. Com fer servir aquest projecte?
Probablement tindràs ja una API implementada i funcionant de manera no segura. Ara tens la tasca de fusionar amb aquest projecte per tal d'autoritzar certes operacions sols a usuaris registrats. És una tasca llarga (que no complicada), però el resultat final serà molt gratificant.