Habang isinusulat ang aking aplikasyon, nakatagpo ako ng kakulangan ng malinaw na mga artikulo kung paano maparehistro ang user sa pamamagitan ng email at mga social network. May magagandang tutorial sa pagse-set up ng klasikong form sa pag-login. Nagkaroon ng magagandang tutorial sa OAuth2 . Mayroong maliit na impormasyon sa kriminal kung paano pagsamahin ang dalawang pamamaraan. Sa panahon ng proseso ng paghahanap, nakagawa kami ng isang magagamit na solusyon. Hindi ito nag-aangkin na ang tunay na katotohanan, ngunit tinutupad nito ang tungkulin nito. Sa artikulong ito ipapakita ko kung paano ipatupad ang isang serbisyo sa pag-iimbak ng tala na may katulad na configuration ng Spring Security mula sa simula. Tandaan: mabuti kung ang mambabasa ay dumaan sa hindi bababa sa ilang mga tutorial sa Spring, dahil ang atensyon ay itutuon lamang sa Spring Security, nang walang detalyadong mga paliwanag ng mga repository, controllers, atbp. Kung hindi, ang isang medyo malaking artikulo ay lalabas sa maging dambuhalang. Nilalaman
Nakita namin na may lumitaw na bagong user sa database. Ang password ay naka-encrypt.
Ang mga tala ay nai-save sa database.
Nakita namin na matagumpay na nailunsad at tumatakbo ang proyekto. Para sa kumpletong kaligayahan, kailangan lang namin ng kakayahang mag-log in sa pamamagitan ng mga social network. Well, magsimula tayo!
Ngayon ang parehong User at OAuth2Authentication ay maaaring kumilos bilang Principal. Dahil isinaalang-alang namin sa UserService nang maaga ang pag-load ng user sa pamamagitan ng data ng Google, gagana ang application para sa parehong uri ng mga user. Binabago namin ang controller ng pangunahing page ng proyekto upang mai-redirect nito ang mga user na naka-log in gamit ang OAuth2 sa page ng mga tala.
Matagumpay na nag-log in ang user sa pamamagitan ng regular na form at sa pamamagitan ng Google account. Ito ang gusto namin! Umaasa ako na ang artikulong ito ay na-clear ang ilang mga punto tungkol sa paglikha ng isang web application, pag-secure nito sa Spring Security, at pagsasama-sama ng iba't ibang paraan ng pag-login. Gamit ang buong code ng proyekto magagawa mo
- Paglikha ng isang Proyekto
- Paglikha ng mga Entity at Application Logic
- Pag-configure ng Spring Security para sa Classic na Pag-login
- Pangunahing configuration SecurityConfig
- Custom na login ng user
- Pagbutihin natin ang controller
- Ilunsad
- Pagse-set up ng OAuth2 gamit ang Google bilang isang halimbawa sa Spring Security
- I-filter ang configuration at application.properties
- Mga highlight ng pagpaparehistro ng isang application sa Google Cloud Platform
- CustomUserInfoTokenServices
- Panghuling paglulunsad ng proyekto
Paglikha ng isang Proyekto
Pumunta kami sa start.spring.io at bumubuo ng batayan ng proyekto:- Web - paglulunsad ng isang application sa built-in na Tomcat, mga url mapping at iba pa;
- JPA - koneksyon sa database;
- Ang bigote ay isang template engine na ginagamit upang makabuo ng mga web page;
- Seguridad - proteksyon ng aplikasyon. Ito ay para sa kung ano ang artikulong ito ay nilikha.
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
Ang configuration ng application.properties ay kasalukuyang ang mga sumusunod:
spring.datasource.url=jdbc:mysql://localhost:3306/springsectut?createDatabaseIfNotExist=true&useSSL=false&autoReconnect=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=yourUsername
spring.datasource.password=yourPassword
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.connection.characterEncoding=utf-8
spring.jpa.properties.connection.CharSet=utf-8
spring.jpa.properties.connection.useUnicode=true
spring.mustache.expose-request-attributes=true
Paglikha ng mga Entity at Application Logic
Mga entidad
Gumawa tayo ng packageentities
kung saan ilalagay natin ang mga entity ng database. Ang user ay ilalarawan ng isang klase User
na nagpapatupad ng interface UserDetails
, na kakailanganin para sa configuration ng Spring Security. Ang user ay magkakaroon ng id, username (email ito), password, pangalan, tungkulin, flag ng aktibidad, pangalan ng Google account at email ( googleName
at googleUsername
).
@Entity
@Table(name = "user")
public class User implements UserDetails
{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String username;
private String password;
private String name;
private boolean active;
private String googleName;
private String googleUsername;
@ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER)
@CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"))
@Enumerated(EnumType.STRING)
private Set<Role> roles;
//Геттеры, сеттеры, toString(), equals(), hashcode(), имплементация UserDetails
}
Ginagamit ang mga tungkulin ng user para i-regulate ang pag-access sa Spring Security. Ang aming aplikasyon ay gagamit lamang ng isang tungkulin:
public enum Role implements GrantedAuthority
{
USER;
@Override
public String getAuthority()
{
return name();
}
}
Gumawa tayo ng klase ng tala na may id, pamagat ng tala, katawan ng tala at id ng user kung kanino ito nabibilang:
@Entity
@Table(name = "note")
public class Note
{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String title;
private String note;
private Long userId;
//Геттеры, сеттеры, toString(), equals(), hashcode()
}
Mga repositoryo
Upang i-save ang mga entity sa database, kailangan namin ng mga repository na gagawa ng lahat ng maruming gawain para sa amin. Gumawa tayo ng isang paketerepos
, sa loob nito gagawa tayo ng mga interface UserRepo
na minana NoteRepo
mula sa interface JpaRepository<Entity, Id>
.
@Service
@Repository
public interface UserRepo extends JpaRepository<User, Long>
{}
@Service
@Repository
public interface NoteRepo extends JpaRepository<Note, Long>
{
List<Note> findByUserId(Long userId);
}
Mga Controller
Ang aming serbisyo sa mga tala ay magkakaroon ng mga sumusunod na pahina:- Tahanan;
- Pagpaparehistro;
- Pagpasok;
- Listahan ng mga tala ng gumagamit.
controllers
na naglalaman ng klase IndexController
na naglalaman ng karaniwang get-mapping ng pangunahing page. Ang klase RegistrationController
ay may pananagutan sa pagpaparehistro ng gumagamit. Ang post-mapping ay kumukuha ng data mula sa form, ini-save ang user sa database at nagre-redirect sa login page. PasswordEncoder
ay ilalarawan sa ibang pagkakataon. Ito ay ginagamit upang i-encrypt ang mga password.
@Controller
public class RegistrationController
{
@Autowired
private UserRepo userRepo;
@Autowired
private PasswordEncoder passwordEncoder;
@GetMapping("/registration")
public String registration()
{
return "registration";
}
@PostMapping("/registration")
public String addUser(String name, String username, String password)
{
User user = new User();
user.setName(name);
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setActive(true);
user.setRoles(Collections.singleton(Role.USER));
userRepo.save(user);
return "redirect:/login";
}
Ang controller na responsable para sa page ng listahan ng mga tala ay kasalukuyang naglalaman ng pinasimpleng functionality, na magiging mas kumplikado pagkatapos ng pagpapatupad ng Spring Security.
@Controller
public class NoteController
{
@Autowired
private NoteRepo noteRepo;
@GetMapping("/notes")
public String notes(Model model)
{
List<Note> notes = noteRepo.findAll();
model.addAttribute("notes", notes);
return "notes";
}
@PostMapping("/addnote")
public String addNote(String title, String note)
{
Note newNote = new Note();
newNote.setTitle(title);
newNote.setNote(note);
noteRepo.save(newNote);
return "redirect:/notes";
}
}
Hindi kami magsusulat ng controller para sa login page dahil ginagamit ito ng Spring Security. Sa halip, kakailanganin namin ng isang espesyal na pagsasaayos. Gaya ng dati, gumawa tayo ng isa pang package, tawagan ito config
, at ilagay ang klase doon MvcConfig
. Kapag isinulat namin ang configuration ng Spring Security, malalaman nito kung aling pahina ang aming tinutukoy kapag ginamit namin ang "/login".
@Configuration
public class MvcConfig implements WebMvcConfigurer
{
public void addViewControllers(ViewControllerRegistry registry)
{
registry.addViewController("/login").setViewName("login");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
}
Mga pahina
Ginagamit ko ang template engine ng Mustache upang lumikha ng mga pahina . Maaari kang magpatupad ng isa pa, hindi mahalaga. Isang meta.mustache file ang ginawa para sa meta information na ginagamit sa lahat ng page. Kasama rin dito ang Bootstrap upang gawing mas maganda ang mga pahina ng aming proyekto. Ang mga pahina ay nilikha sa direktoryo ng "src/main/resources/templates". Ang mga file ay may extension na bigote. Ang paglalagay ng html code nang direkta sa artikulo ay gagawin itong masyadong malaki, kaya narito ang isang link sa folder ng mga template sa GitHub repository ng proyekto .Pag-configure ng Spring Security para sa Classic na Pag-login
Tinutulungan kami ng Spring Security na protektahan ang application at ang mga mapagkukunan nito mula sa hindi awtorisadong pag-access. Gagawa tayo ng isang maigsi na pagsasaayos ng pagtatrabaho sa isang klaseSecurityConfig
na minana mula sa WebSecurityConfigurerAdapter
, na ilalagay natin sa pakete config
. Markahan natin ito ng @EnableWebSecurity annotation, na magbibigay-daan sa suporta sa Spring Security, at ang @Configuration annotation, na nagpapahiwatig na ang klase na ito ay naglalaman ng ilang configuration. Tandaan: ang awtomatikong na-configure na pom.xml ay naglalaman ng bersyon ng Spring Boot parent component 2.1.4.RELEASE, na pumigil sa Security na maipatupad sa itinatag na paraan. Upang maiwasan ang mga salungatan sa proyekto, inirerekumenda na baguhin ang bersyon sa 2.0.1.RELEASE.
Pangunahing configuration SecurityConfig
Ang aming configuration ay magagawang:-
I-encrypt ang mga password gamit ang
BCryptPasswordEncoder
:@Autowired private PasswordEncoder passwordEncoder; @Bean PasswordEncoder passwordEncoder() { PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); return passwordEncoder; }
-
Mag-log in gamit ang isang espesyal na nakasulat na provider ng pagpapatunay:
@Autowired private AuthProvider authProvider; @Override protected void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(authProvider); }
-
Payagan ang mga hindi kilalang user na ma-access ang home page, registration at login page. Ang lahat ng iba pang mga kahilingan ay dapat isagawa ng mga naka-log in na user. Italaga natin ang naunang inilarawan na "/login" bilang pahina sa pag-login. Kung matagumpay ang pag-login, dadalhin ang user sa isang page na may listahan ng mga tala; kung may error, mananatili ang user sa login page. Sa matagumpay na paglabas, dadalhin ang user sa pangunahing pahina.
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/resources/**", "/", "/login**", "/registration").permitAll() .anyRequest().authenticated() .and().formLogin().loginPage("/login") .defaultSuccessUrl("/notes").failureUrl("/login?error").permitAll() .and().logout().logoutSuccessUrl("/").permitAll(); }
Custom na login ng user
Ang isang self-writtenAuthProvider
ay magbibigay-daan sa gumagamit na mag-log in hindi lamang sa pamamagitan ng email, kundi pati na rin sa pamamagitan ng username.
@Component
public class AuthProvider implements AuthenticationProvider
{
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
String username = authentication.getName();
String password = (String) authentication.getCredentials();
User user = (User) userService.loadUserByUsername(username);
if(user != null && (user.getUsername().equals(username) || user.getName().equals(username)))
{
if(!passwordEncoder.matches(password, user.getPassword()))
{
throw new BadCredentialsException("Wrong password");
}
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
return new UsernamePasswordAuthenticationToken(user, password, authorities);
}
else
throw new BadCredentialsException("Username not found");
}
public boolean supports(Class<?> arg)
{
return true;
}
}
Tulad ng maaaring napansin mo, ang klase UserService
na matatagpuan sa package ay may pananagutan sa pag-load ng user services
. Sa aming kaso, naghahanap ito ng user hindi lamang sa pamamagitan ng field username
, tulad ng built-in na pagpapatupad, kundi pati na rin sa pamamagitan ng user name, pangalan ng Google account at email ng Google account. Ang huling dalawang pamamaraan ay magiging kapaki-pakinabang sa amin kapag nagpapatupad ng pag-login sa pamamagitan ng OAuth2. Narito ang klase ay ibinigay sa isang pinaikling bersyon.
@Service
public class UserService implements UserDetailsService
{
@Autowired
private UserRepo userRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
User userFindByUsername = userRepo.findByUsername(username);
//Остальные поиски
if(userFindByUsername != null)
{
return userFindByUsername;
}
//Остальные проверки
return null;
}
}
Tandaan: huwag kalimutang isulat ang mga kinakailangang pamamaraan sa UserRepo
!
Pagbutihin natin ang controller
Na-configure namin ang Spring Security. Ngayon na ang oras upang samantalahin ito sa iyong controller ng mga tala. Ngayon, ang bawat pagmamapa ay tatanggap ng karagdagang Principal parameter, kung saan susubukan nitong hanapin ang user. Bakit hindi ako direktang makapag-inject ng klaseUser
? Pagkatapos ay magkakaroon ng salungatan dahil sa hindi pagkakatugma ng mga uri ng user kapag sumulat kami ng login sa pamamagitan ng mga social network. Nagbibigay kami ng kinakailangang flexibility nang maaga. Ang aming code ng controller ng mga tala ay ganito na ngayon:
@GetMapping("/notes")
public String notes(Principal principal, Model model)
{
User user = (User) userService.loadUserByUsername(principal.getName());
List<Note> notes = noteRepo.findByUserId(user.getId());
model.addAttribute("notes", notes);
model.addAttribute("user", user);
return "notes";
}
@PostMapping("/addnote")
public String addNote(Principal principal, String title, String note)
{
User user = (User) userService.loadUserByUsername(principal.getName());
Note newNote = new Note();
newNote.setTitle(title);
newNote.setNote(note);
newNote.setUserId(user.getId());
noteRepo.save(newNote);
return "redirect:/notes";
}
Tandaan: ang proyekto ay may naka-enable na proteksyon ng CSRF bilang default , kaya i-disable ito para sa iyong sarili (http.csrf().disable()), o huwag kalimutan, bilang may-akda ng artikulo, na magdagdag ng nakatagong field na may csrf token sa lahat ng post requests.
Ilunsad
Sinusubukan naming ilunsad ang proyekto.Pagse-set up ng OAuth2 gamit ang Google bilang isang halimbawa sa Spring Security
Kapag nagpapatupad ng OAuth2, umasa ako sa opisyal na tutorial na ito mula sa Spring . Upang suportahan ang OAuth2, idagdag ang sumusunod na library sa pom.xml:<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
Baguhin natin ang configuration ng Spring Security natin sa SecurityConfig
. Una, idagdag natin ang @EnableOAuth2Client annotation. Awtomatiko nitong kukunin kung ano ang kailangan mong mag-log in sa pamamagitan ng mga social network.
I-filter ang configuration at application.properties
Mag-inject tayo ng OAuth2ClientContext na gagamitin sa aming configuration ng seguridad.@Autowired
private OAuth2ClientContext oAuth2ClientContext;
Ginagamit ang OAuth2ClientContext kapag gumagawa ng filter na nagpapatunay sa kahilingan sa social login ng user. Available ang filter salamat sa @EnableOAuth2Client annotation. Ang kailangan lang nating gawin ay tawagan ito sa tamang pagkakasunud-sunod, bago ang pangunahing filter ng Spring Security. Pagkatapos lamang ay makakahuli kami ng mga pag-redirect sa panahon ng proseso ng pag-log in gamit ang OAuth2. Upang gawin ito, ginagamit namin ang FilterRegistrationBean
, kung saan itinakda namin ang priyoridad ng aming filter sa -100.
@Bean
public FilterRegistrationBean oAuth2ClientFilterRegistration(OAuth2ClientContextFilter oAuth2ClientContextFilter)
{
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(oAuth2ClientContextFilter);
registration.setOrder(-100);
return registration;
}
private Filter ssoFilter()
{
OAuth2ClientAuthenticationProcessingFilter googleFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/google");
OAuth2RestTemplate googleTemplate = new OAuth2RestTemplate(google(), oAuth2ClientContext);
googleFilter.setRestTemplate(googleTemplate);
CustomUserInfoTokenServices tokenServices = new CustomUserInfoTokenServices(googleResource().getUserInfoUri(), google().getClientId());
tokenServices.setRestTemplate(googleTemplate);
googleFilter.setTokenServices(tokenServices);
tokenServices.setUserRepo(userRepo);
tokenServices.setPasswordEncoder(passwordEncoder);
return googleFilter;
}
Kailangan mo ring magdagdag ng bagong filter sa configure(HttpSecurity http) function:
http.addFilterBefore(ssoFilter(), UsernamePasswordAuthenticationFilter.class);
Kailangan ding malaman ng filter na nakarehistro ang kliyente sa pamamagitan ng Google. Ang @ConfigurationProperties annotation ay tumutukoy kung aling mga katangian ng configuration ang hahanapin sa application.properties.
@Bean
@ConfigurationProperties("google.client")
public AuthorizationCodeResourceDetails google()
{
return new AuthorizationCodeResourceDetails();
}
Upang makumpleto ang pagpapatunay, kailangan mong tukuyin ang endpoint ng impormasyon ng user ng Google:
@Bean
@ConfigurationProperties("google.resource")
public ResourceServerProperties googleResource()
{
return new ResourceServerProperties();
}
Nang mairehistro ang aming application sa Google Cloud Platform , magdaragdag kami ng mga property na may naaangkop na prefix sa application.properties:
google.client.clientId=yourClientId
google.client.clientSecret=yourClientSecret
google.client.accessTokenUri=https://www.googleapis.com/oauth2/v4/token
google.client.userAuthorizationUri=https://accounts.google.com/o/oauth2/v2/auth
google.client.clientAuthenticationScheme=form
google.client.scope=openid,email,profile
google.resource.userInfoUri=https://www.googleapis.com/oauth2/v3/userinfo
google.resource.preferTokenInfo=true
Mga highlight ng pagpaparehistro ng isang application sa Google Cloud Platform
Path: Mga API at Serbisyo -> Window ng Kahilingan sa Pag-access ng Mga Kredensyal OAuth:- Pangalan ng application: Spring login form at OAuth2 tutorial
- Suporta sa email address: ang iyong email
- Saklaw ng Google API: email, profile, openid
- Mga awtorisadong domain: me.org
- Mag-link sa pangunahing pahina ng aplikasyon: http://me.org:8080
- Link sa patakaran sa privacy ng app: http://me.org:8080
- Link sa mga tuntunin ng paggamit ng application: http://me.org:8080
- Uri: Web Application
- Pamagat: Spring login form at OAuth2 tutorial
- Pinapayagan ang mga mapagkukunan ng JavaScript: http://me.org, http://me.org:8080
- Mga pinapayagang URI sa pag-redirect: http://me.org:8080/login, http://me.org:8080/login/google
CustomUserInfoTokenServices
Napansin mo ba ang salitang Custom sa paglalarawan ng function ng filter? KlaseCustomUserInfoTokenServices
. Oo, gagawa kami ng sarili naming klase na may blackjack at ang kakayahang i-save ang user sa database! Gamit ang Ctrl-N keyboard shortcut sa IntelliJ IDEA, mahahanap at makikita mo kung paano UserInfoTokenServices
ipinapatupad ang default. Kopyahin natin ang code nito sa bagong likhang klase CustomUserInfoTokenServices
. Karamihan sa mga ito ay maaaring iwanang hindi nagbabago. Bago baguhin ang lohika ng mga function, idagdag natin UserRepo
at bilang mga pribadong field ng klase PasswordEncoder
. Gumawa tayo ng mga setter para sa kanila. Idagdag natin ang @Autowired UserRepo userRepo sa klase ng SecurityConfig. Tinitingnan namin kung paano nawawala ang pointer sa error sa paraan ng paggawa ng filter, at natutuwa kami. Bakit hindi direktang mailapat ang @Autowired sa CustomUserInfoTokenServices? Dahil ang klase na ito ay hindi kukuha ng dependency, dahil ito mismo ay hindi minarkahan ng anumang Spring annotation, at ang constructor nito ay tahasang nilikha kapag ang filter ay ipinahayag. Alinsunod dito, ang mekanismo ng DI ng Spring ay hindi alam ang tungkol dito. Kung i-annotate namin ang @Autowired sa anumang bagay sa klase na ito, makakakuha kami ng NullPointerException kapag ginamit. Ngunit sa pamamagitan ng mga tahasang setter ay gumagana nang maayos ang lahat. Pagkatapos ipatupad ang mga kinakailangang bahagi, ang pangunahing bagay ng interes ay nagiging function na loadAuthentication, kung saan ang Map<String, Object> na may impormasyon tungkol sa user ay nakuha. Sa proyektong ito ko ipinatupad ang pag-save ng isang user na naka-log in sa pamamagitan ng isang social network sa database. Dahil gumagamit kami ng Google account bilang OAuth2 provider, tinitingnan namin kung ang mapa ay naglalaman ng field na "sub" na karaniwan para sa Google. Kung ito ay naroroon, nangangahulugan ito na ang impormasyon tungkol sa gumagamit ay natanggap nang tama. Lumilikha kami ng bagong user at i-save ito sa database.
@Override
public OAuth2Authentication loadAuthentication(String accessToken)
throws AuthenticationException, InvalidTokenException
{
Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
if(map.containsKey("sub"))
{
String googleName = (String) map.get("name");
String googleUsername = (String) map.get("email");
User user = userRepo.findByGoogleUsername(googleUsername);
if(user == null)
{
user = new User();
user.setActive(true);
user.setRoles(Collections.singleton(Role.USER));
}
user.setName(googleName);
user.setUsername(googleUsername);
user.setGoogleName(googleName);
user.setGoogleUsername(googleUsername);
user.setPassword(passwordEncoder.encode("oauth2user"));
userRepo.save(user);
}
if (map.containsKey("error"))
{
this.logger.debug("userinfo returned error: " + map.get("error"));
throw new InvalidTokenException(accessToken);
}
return extractAuthentication(map);
}
Kapag gumagamit ng ilang provider, maaari mong tukuyin ang iba't ibang mga opsyon sa isang CustomUserInfoTokenServices, at magrehistro ng iba't ibang klase ng mga katulad na serbisyo sa paraan ng pagdeklara ng filter.
@GetMapping("/")
public String index(Principal principal)
{
if(principal != null)
{
return "redirect:/notes";
}
return "index";
}
GO TO FULL VERSION