* BAEL-509: Initial Commit - working but needs a few fixes to REST API, etc.

* Fixed Authentication Failure - added subscription handlers - sufficient for Websocket Authentication/Authorization - still some issues to resolve with subscriptions and REST API

* Final version

* CSRF token controller - cleanup of chat wrapper
This commit is contained in:
Adam InTae Gerard
2017-07-04 03:57:27 +01:00
committed by KevinGilmore
parent 70ae331cd7
commit db2bb252de
47 changed files with 1654 additions and 0 deletions
@@ -0,0 +1,56 @@
package com.baeldung.springsecuredsockets.config;
import org.h2.tools.Server;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.resource.PathResourceResolver;
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
import java.sql.SQLException;
@Configuration
@EnableWebMvc
@EnableJpaRepositories
@ComponentScan("com.baeldung.springsecuredsockets")
@Import({ SecurityConfig.class, DataStoreConfig.class, SocketBrokerConfig.class, SocketSecurityConfig.class })
public class AppConfig extends WebMvcConfigurerAdapter {
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/login").setViewName("login");
registry.addViewController("/secured/socket").setViewName("socket");
registry.addViewController("/secured/success").setViewName("success");
registry.addViewController("/denied").setViewName("denied");
}
@Bean
public UrlBasedViewResolver viewResolver() {
final UrlBasedViewResolver bean = new UrlBasedViewResolver();
bean.setPrefix("/WEB-INF/jsp/");
bean.setSuffix(".jsp");
bean.setViewClass(JstlView.class);
return bean;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/", "/resources/")
.setCachePeriod(3600)
.resourceChain(true)
.addResolver(new PathResourceResolver());
}
// View H2
@Bean(initMethod="start", destroyMethod="stop")
public Server h2Console () throws SQLException {
return Server.createWebServer("-web","-webAllowOthers","-webDaemon","-webPort", "8082");
}
}
@@ -0,0 +1,69 @@
package com.baeldung.springsecuredsockets.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@ComponentScan("com.baeldung.springsecuredsockets")
@EnableTransactionManagement
@EnableJpaRepositories("com.baeldung.springsecuredsockets.repositories")
public class DataStoreConfig {
//Configuration for embededded data store through H2
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.setName("socketDB")
.addScript("classpath:schema.sql")
.addScript("classpath:data.sql")
.setScriptEncoding("UTF-8")
.continueOnError(true)
.ignoreFailedDrops(true)
.build();
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
final HibernateJpaVendorAdapter bean = new HibernateJpaVendorAdapter();
bean.setDatabase(Database.H2);
bean.setGenerateDdl(true);
return bean;
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
final LocalContainerEntityManagerFactoryBean bean = new LocalContainerEntityManagerFactoryBean();
bean.setDataSource(dataSource);
bean.setJpaVendorAdapter(jpaVendorAdapter());
bean.setPackagesToScan("com.baeldung.springsecuredsockets");
//Set properties on Hibernate
Properties properties = new Properties();
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
properties.setProperty("hibernate.hbm2ddl.auto", "update");
bean.setJpaProperties(properties);
return bean;
}
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
}
@@ -0,0 +1,136 @@
package com.baeldung.springsecuredsockets.config;
import com.baeldung.springsecuredsockets.security.CustomAccessDeniedHandler;
import com.baeldung.springsecuredsockets.security.CustomDaoAuthenticationProvider;
import com.baeldung.springsecuredsockets.security.CustomLoginSuccessHandler;
import com.baeldung.springsecuredsockets.security.CustomLogoutSuccessHandler;
import com.baeldung.springsecuredsockets.security.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
/**
* @EnableGlobalAuthentication annotates:
* @EnableWebSecurity
* @EnableWebMvcSecurity
* @EnableGlobalMethodSecurity Passing in 'prePostEnabled = true' allows:
* <p>
* Pre/Post annotations such as:
* @PreAuthorize("hasRole('ROLE_USER')")
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@EnableWebSecurity
@ComponentScan("com.baeldung.springsecuredsockets")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService customUserDetailsService;
/**
* Login, Logout, Success, and Access Denied beans/handlers
*/
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return new CustomAccessDeniedHandler();
}
@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return new CustomLogoutSuccessHandler();
}
@Bean
public AuthenticationSuccessHandler loginSuccessHandler() {
return new CustomLoginSuccessHandler();
}
/**
* Authentication beans
*/
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
final DaoAuthenticationProvider bean = new CustomDaoAuthenticationProvider();
bean.setUserDetailsService(customUserDetailsService);
bean.setPasswordEncoder(encoder());
return bean;
}
/**
* Order of precedence is very important.
* <p>
* Matching occurs from top to bottom - so, the topmost match succeeds first.
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/index", "/authenticate")
.permitAll()
.antMatchers("/secured/**/**",
"/secured/success", "/secured/socket", "/secured/success")
.authenticated()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/authenticate")
.successHandler(loginSuccessHandler())
.failureUrl("/denied").permitAll()
.and()
.logout()
.logoutSuccessHandler(logoutSuccessHandler())
.and()
/**
* Applies to User Roles - not to login failures or unauthenticated access attempts.
*/
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler())
.and()
.authenticationProvider(authenticationProvider());
/** Disabled for local testing */
http
.csrf().disable();
/** This is solely required to support H2 console viewing in Spring MVC with Spring Security */
http
.headers()
.frameOptions()
.disable();
}
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**");
}
}
@@ -0,0 +1,27 @@
package com.baeldung.springsecuredsockets.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
@ComponentScan("com.baeldung.springsecuredsockets.controllers")
public class SocketBrokerConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/secured/history");
config.setApplicationDestinationPrefixes("/spring-security-mvc-socket");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/secured/chat").withSockJS();
}
}
@@ -0,0 +1,21 @@
package com.baeldung.springsecuredsockets.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
@Configuration
public class SocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected boolean sameOriginDisabled() {
return true;
}
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpDestMatchers("/secured/**").authenticated()
.anyMessage().authenticated();
}
}
@@ -0,0 +1,24 @@
package com.baeldung.springsecuredsockets.config;
import org.hibernate.cfg.Configuration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
public class WebAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) throws ServletException {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
context.setServletContext(container);
ServletRegistration.Dynamic servlet = container.addServlet("dispatcher", new DispatcherServlet(context));
servlet.setLoadOnStartup(1);
servlet.addMapping("/");
}
}
@@ -0,0 +1,19 @@
package com.baeldung.springsecuredsockets.controllers;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
@Controller
public class CsrfTokenController {
@RequestMapping(value = "/csrf", method = RequestMethod.GET)
public @ResponseBody
String getCsrfToken(HttpServletRequest request) {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
return csrf.getToken();
}
}
@@ -0,0 +1,28 @@
package com.baeldung.springsecuredsockets.controllers;
import com.baeldung.springsecuredsockets.transfer.socket.Message;
import com.baeldung.springsecuredsockets.transfer.socket.OutputMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import java.text.SimpleDateFormat;
import java.util.Date;
@Controller
public class SocketController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
private static final Logger log = LoggerFactory.getLogger(SocketController.class);
@MessageMapping("/secured/chat")
@SendTo("/secured/history")
public OutputMessage send(Message msg) throws Exception {
OutputMessage out = new OutputMessage(msg.getFrom(), msg.getText(), new SimpleDateFormat("HH:mm").format(new Date()));
return out;
}
}
@@ -0,0 +1,51 @@
package com.baeldung.springsecuredsockets.domain;
import javax.persistence.*;
import java.util.Set;
@Entity
@Table(name = "role")
public class Role {
@Id
//Slight increase in performance over GenerationType.IDENTITY
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "role_id", updatable = false, nullable = false)
private long role_id;
@Column(name = "role", nullable = false)
private String role;
/**
* Many to Many Example - see Role.
* <p>
* One User many have many Roles.
* Each Role may be assigned to many Users.
*/
@ManyToMany(mappedBy = "roles", fetch = FetchType.EAGER)
private Set<User> users;
public long getRole_id() {
return role_id;
}
public void setRole_id(long role_id) {
this.role_id = role_id;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
}
@@ -0,0 +1,65 @@
package com.baeldung.springsecuredsockets.domain;
import javax.persistence.*;
import java.util.Set;
//Custom User Model
@Entity
@Table(name = "user")
public class User {
@Id
//Slight increase in performance over GenerationType.IDENTITY
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "user_id", updatable = false, nullable = false)
private long user_id;
@Column(name = "username", nullable = false)
private String username;
@Column(name = "password", nullable = false)
private String password;
/**
* Many to Many Example - see Role.
* <p>
* One User many have many Roles.
* Each Role may be assigned to many Users.
*/
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles;
public long getUser_id() {
return user_id;
}
public void setUser_id(long user_id) {
this.user_id = user_id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}
@@ -0,0 +1,11 @@
package com.baeldung.springsecuredsockets.repositories;
import com.baeldung.springsecuredsockets.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
@@ -0,0 +1,23 @@
package com.baeldung.springsecuredsockets.security;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Applies to User Roles - not to login failures or unauthenticaed access attempts.
*/
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exc) throws IOException, ServletException {
response.setStatus(HttpStatus.BAD_REQUEST.value());
response.sendRedirect(request.getContextPath() + "/denied");
}
}
@@ -0,0 +1,42 @@
package com.baeldung.springsecuredsockets.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
public class CustomDaoAuthenticationProvider extends DaoAuthenticationProvider {
Logger log = LoggerFactory.getLogger(CustomDaoAuthenticationProvider.class);
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails u = null;
try {
u = getUserDetailsService().loadUserByUsername(name);
} catch (UsernameNotFoundException ex) {
log.error("User '" + name + "' not found");
} catch (Exception e) {
log.error("Exception in CustomDaoAuthenticationProvider: " + e);
}
if (u != null) {
if (u.getPassword().equals(password)) {
return new UsernamePasswordAuthenticationToken(u, password, u.getAuthorities());
}
}
throw new BadCredentialsException(messages.getMessage("CustomDaoAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
@@ -0,0 +1,25 @@
package com.baeldung.springsecuredsockets.security;
import com.baeldung.springsecuredsockets.domain.User;
import com.baeldung.springsecuredsockets.repositories.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
UserRepository userRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
User user = userRepository.findByUsername(authentication.getName());
response.setStatus(HttpStatus.OK.value());
response.sendRedirect(request.getContextPath() + "/secured/success");
}
}
@@ -0,0 +1,21 @@
package com.baeldung.springsecuredsockets.security;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
response.setStatus(HttpStatus.OK.value());
response.sendRedirect(request.getContextPath() + "/index");
}
}
@@ -0,0 +1,51 @@
package com.baeldung.springsecuredsockets.security;
import com.baeldung.springsecuredsockets.domain.Role;
import com.baeldung.springsecuredsockets.domain.User;
import com.baeldung.springsecuredsockets.repositories.UserRepository;
import com.baeldung.springsecuredsockets.transfer.user.CustomUserDetails;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.HashSet;
@Service()
public class CustomUserDetailsService implements UserDetailsService {
Logger log = LoggerFactory.getLogger(CustomUserDetailsService.class);
@Autowired
private UserRepository userRepository;
public CustomUserDetailsService() {
super();
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
User user = userRepository.findByUsername(username);
if (user != null) return new CustomUserDetails(user, getAuthorities(user));
} catch (Exception ex) {
log.error("Exception in CustomUserDetailsService: " + ex);
}
return null;
}
private Collection<GrantedAuthority> getAuthorities(User user) {
Collection<GrantedAuthority> authorities = new HashSet<>();
for (Role role : user.getRoles()) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRole());
authorities.add(grantedAuthority);
}
return authorities;
}
}
@@ -0,0 +1,12 @@
package com.baeldung.springsecuredsockets.security;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
/**
* This is required to enable springSecurityFilterChain.
*
* Remember that Spring Security utilizes filters to intercept and manage requests
* according to the specified authorization and authentication rules
*/
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {}
@@ -0,0 +1,20 @@
package com.baeldung.springsecuredsockets.transfer.socket;
public class Message {
private String from;
private String text;
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
@@ -0,0 +1,17 @@
package com.baeldung.springsecuredsockets.transfer.socket;
public class OutputMessage extends Message {
private String time;
public OutputMessage(final String from, final String text, final String time) {
setFrom(from);
setText(text);
this.time = time;
}
public String getTime() {
return time;
}
public void setTime(String time) { this.time = time; }
}
@@ -0,0 +1,25 @@
package com.baeldung.springsecuredsockets.transfer.user;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
public class CustomUserDetails extends User {
private com.baeldung.springsecuredsockets.domain.User user;
public CustomUserDetails(com.baeldung.springsecuredsockets.domain.User user, Collection<? extends GrantedAuthority> authorities) {
super(user.getUsername(), user.getPassword(), authorities);
this.user = user;
}
public CustomUserDetails(com.baeldung.springsecuredsockets.domain.User user, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(user.getUsername(), user.getPassword(), enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.user = user;
}
public com.baeldung.springsecuredsockets.domain.User getUser() {
return user;
}
}