I am trying to disable spring security for request to frontend. I am using oauth2-auto-configuration as a dependency, also i have removed spring starter security from maven. Also i need to say that everything is working from Postman.
Login required when requesting API's from frontend
Security Config
#Configuration
#EnableWebSecurity(debug = true)
#Order(SecurityProperties.BASIC_AUTH_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public static PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
#Autowired
public UserDetailsService userDetailsService;
#Autowired
private PasswordEncoder passwordEncoder;
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers("/","/**").permitAll()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.and().httpBasic().disable();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
application.properties
spring.data.mongodb.database=music-test-oauth2-2
security.oauth2.resource.id=oauth2_application
access_token.validity_period=3600
refresh_token.validity_period=10000
dependencies from pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.5</version>
</dependency>
</dependencies>
CORS Filter
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
};
}
Thanks.
Here is a simple CORF-Filter that you can try:
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
response.setHeader("Access-Control-Max-Age", "3600");
if ("OPTIONS".equalsIgnoreCase(((HttpServletRequest) req).getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
#Override
public void destroy() {
}
#Override
public void init(FilterConfig config) throws ServletException {
}
}
Maybe your request Headers are not correct, try to use this code:
public obtainAccessToken(username: string, password: string): Promise<void> {
let params = new URLSearchParams();
params.append('username', username);
params.append('password', password);
params.append('grant_type','password');
params.append('client_id','fooClientIdPassword');
let headers = new Headers({'Content-type': 'application/x-www-form-urlencoded; charset=utf-8', 'Authorization': 'Basic '+btoa("fooClientIdPassword:secret")});
let options = new RequestOptions({ headers: headers });
return this.http.post(environment.apiEndpointAuth + 'user/login', params.toString(), options)
.toPromise().then(response => {
this.saveToken(response.json());
})
.catch(this.handleError);
}
Implement Following CORS class and see if it's working-
#Component
public class CorsFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (exchange != null) {
exchange.getResponse().getHeaders().add("Access-Control-Allow-Origin", "*");
exchange.getResponse().getHeaders().add("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS");
exchange.getResponse().getHeaders().add("Access-Control-Allow-Headers",
"DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range");
exchange.getResponse().getHeaders().add("Access-Control-Max-Age", "1728000");
if (exchange.getRequest().getMethod() == HttpMethod.OPTIONS) {
exchange.getResponse().getHeaders().add("Access-Control-Max-Age", "1728000");
exchange.getResponse().setStatusCode(HttpStatus.NO_CONTENT);
return Mono.empty();
} else {
exchange.getResponse().getHeaders().add("Access-Control-Expose-Headers", "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range");
return chain.filter(exchange);
}
} else {
return chain.filter(exchange);
}
}
}
Second you need to scan CorsFilter class for that in you main class do following-
#ComponentScan(value = "com.cors.filter", // Your CorsFilter class package path
useDefaultFilters = false)
public class MainClass {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.
run(MainClass.class, args);
}
}
Update:
Add Following dependency in pom.xml
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.1.4.RELEASE</version>
</dependency>
The problem was that i should have to pass the content type on headers as 'application/x-www-form-urlencoded' also i have to pass the Basic Auth to header generated from client_id also client_secert.
Thanks :)
Related
when iam adding the Spring Boot Starter Security dependency on my Projekt my STOMP Endpoint responding an 404 Code to my React frontend. I build a simple Demo Projekt with only web socket dependency. In this case everything works fine. When iam adding the security dependency without any configuration i get a 403. At this point everything is fine. When iam adding the same WebSecurityConfigurerAdapter implementation as the Main Projekt everything works fine aswell. But on my main Projekt it did not work. Everytime i get a 404 on my endpoint ws://localhost:8080/socket
I tried to get this work for one Week now... I cant figure it out where i should configure the Security part for the Sockets
The goul of all this is to stream progress information of some Tasks to the frontend. If you have any other solutions to build that i would be happy. It could be that websockets are not the best way to do that.
and btw. its my first Question on Stackoverflow please dont judge me if the formatting is not the best way :-)
Iam storing the User Informations in a h2 Database.
Here my Configurations and Dependencys for the Backend
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<version>2.6.6</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.0</version>
</dependency>
The WebSecurityConfigurerAdapter implementation
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final AppUserDetailService appUserDetailService;
private final Filter jwtAuthFilter;
#Autowired
public SecurityConfig(AppUserDetailService appUserDetailService, Filter jwtAuthFilter){
this.appUserDetailService = appUserDetailService;
this.jwtAuthFilter = jwtAuthFilter;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(appUserDetailService);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
.antMatchers("/auth/**","/oauth/**", "/topic/**", "/socket/**", "/app/**").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/**").permitAll().and()
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception{
return super.authenticationManagerBean();
}
}
JWTAuthFilter
#Slf4j
#Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JWTUtilService jwtUtil;
public JwtAuthFilter(JWTUtilService jwtUtil) {
this.jwtUtil = jwtUtil;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = getAuthToken(request);
try{
if(token != null && !token.isBlank()){
String username = jwtUtil.extractUsername(token);
setSecurityContext(username);
}
}catch (Exception e){
log.error("No valid Token found!", e);
}
filterChain.doFilter(request, response);
}
private void setSecurityContext(String username) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, "", List.of());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
private String getAuthToken(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if(authHeader != null){
return authHeader.replace("Bearer", "").trim();
}
return null;
}
}
AppUserDetailsService implementation
#Service
public class AppUserDetailService implements UserDetailsService {
private final AppUserRepo appUserRepo;
private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
private final JWTUtilService jwtUtilService;
public AppUserDetailService(AppUserRepo appUserRepo, JWTUtilService jwtUtilService) {
this.appUserRepo = appUserRepo;
this.jwtUtilService = jwtUtilService;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return appUserRepo.findByUsername(username)
.map(appUser -> User
.withUsername(username)
.password(appUser.getPassword())
.authorities("user")
.build())
.orElseThrow(()-> new UsernameNotFoundException("Username does not exist: "+username));
}
public String registerUser(AppUserDTO user) {
if(!userExisting(user)){
user.setPassword(encoder.encode(user.getPassword()));
appUserRepo.save(user);
return jwtUtilService.createToken(new HashMap<>(), user.getUsername());
}else{
throw new UserExistsException("User is currently existing.");
}
}
public boolean userExisting(AppUserDTO user){
return appUserRepo.findByUsername(user.getUsername()).isPresent();
}
}
WebSocketMessageBrokerConfigurer implementation
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket").setAllowedOriginPatterns("*");
}
}
Here my simplified version of the implementation of the React frontend
Top Level Component
import {StompSessionProvider} from "react-stomp-hooks";
<StompSessionProvider url={"ws://localhost:8080/socket"} topics={['/topic/progress']} onConnect={()=>{console.log("Connected")}} onDisconnect={()=>{console.log("Disconnected")}} onError={(err)=>{console.log(err)}}>
<Home/>
</StompSessionProvider>
Home Component
import {useSubscription} from "react-stomp-hooks";
export default function ZapContinousHome() {
useSubscription("/topic/progress", (message) => setMessage(message.body));
return(
<h1>Home</h1>
)
}
I am building a new project with Spring boot 2.4 + spring security + jwt + React. We created a login page in react and react build is in static folder.
please find the code below:
My security.java
#Configuration
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Autowired
UserDetailServiceImpl userDetails;
#Autowired
JWTAuthenticationFilter jwtRequestFilter;
#Autowired
public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetails);
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.cors().and().csrf().disable().authorizeRequests()
.antMatchers("/login/**, /resources/**").permitAll()
.antMatchers("/authenticate/**").permitAll()
.anyRequest().authenticated()
.and().exceptionHandling()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public PasswordEncoder getPasswordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
In UserDetailServiceImpl we are fetching users from mysql db in
loadUserByUsername(String username)
JWTAuthenticationFilter.java
#Component
public class JWTAuthenticationFilter extends OncePerRequestFilter {
#Autowired
private UserDetailServiceImpl userDetailService;
#Autowired
private JwtTokenUtil jwtUtil;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
filterChain.doFilter(request, response);
}
}
JwtTokenUtil.java
#Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -3301605591108950415L;
static final String CLAIM_KEY_USERNAME = "sub";
static final String CLAIM_KEY_AUDIENCE = "aud";
static final String CLAIM_KEY_CREATED = "iat";
static final String AUDIENCE_UNKNOWN = "unknown";
static final String AUDIENCE_WEB = "web";
static final String AUDIENCE_MOBILE = "mobile";
static final String AUDIENCE_TABLET = "tablet";
//#Value("${jwt.secret}")
private String secret="ThisIsASecret";
//#Value("${jwt.expiration}")
private Long expiration=604800L;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(SignatureAlgorithm.HS256, secret).compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
My controller:
#RestController
public class LoginController {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private JwtTokenUtil jwtTokenUtil;
#Autowired
private UserDetailServiceImpl userDetailsService;
#GetMapping(value = "/welcome" )
public String welcome(HttpServletRequest request) throws Exception {
return "login sucess";
}
#PostMapping(value = "/authenticate")
public ResponseEntity<?> createAuthenticationToken(#RequestBody AccountCredentials authenticationRequest) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
);
}
catch (BadCredentialsException e) {
throw new Exception("Incorrect username or password", e);
}
final UserDetails userDetails = userDetailsService
.loadUserByUsername(authenticationRequest.getUsername());
final String jwt = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthenticationResponse(jwt));
}
}
/authenticate is giving forbidden, tested from postman.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>war</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.cmp</groupId>
<artifactId>project</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>project</name>
<description>Project boot react</description>
<properties>
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency> -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- <dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency> -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
We are looking for login page to appears and generate jwt + pass all security. it says forbidden for /authenticate api from postman and /login is not found (login page is created in react and build is in resources/static folder). Note: we are running in single server i.e. tomcat 9 + java 11. There is not exceptions in the console.
I figure it out. there are few changes in code.
class JWTAuthenticationFilter -> change
UsernamePasswordAuthenticationToken parameters userDetails to
userDetails.getUserName();
class WebSecurity -> Added passwordEncoder(getPasswordEncoder());
Please comment if anyone needs help to implement it. Thank you
Initially, I started the project with IntelliJ Idea Tomcat Local Server configuration (everything worked), but decided to use Spring Boot Application. I added the main class, changed pom.xml (delete spring-context and add spring-boot-starter-parent, spring-boot, spring-boot-starter-tomcat, spring-boot-starter-web, spring-boot-autoconfigure), after that application runs , GET-Method works, but POST - not supported. Help me please!!! Thank you!
Main.class
#SpringBootApplication(scanBasePackageClasses = {SpringConfig.class})
public class Main extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
Pom.xml to springBoot
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
Main configuration class
#Configuration
#ComponentScan("ru")
#PropertySource("classpath:application.properties")
#EnableWebMvc
public class SpringConfig implements WebMvcConfigurer {
private final ApplicationContext applicationContext;
#Autowired
public SpringConfig(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/*
* Dispatcher configuration for serving static resources
*/
#Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
WebMvcConfigurer.super.addResourceHandlers(registry);
registry.addResourceHandler("/images/**").addResourceLocations("/images/");
registry.addResourceHandler("/webjars/bootstrap/4.6.0/css/**").addResourceLocations("/webjars/bootstrap/4.6.0/css/bootstrap.min.css");
registry.addResourceHandler("assets/select2-develop/**").addResourceLocations("/assets/");
registry.addResourceHandler("/css/**").addResourceLocations("/css/");
registry.addResourceHandler("/js/**").addResourceLocations("/js/");
}
/*
* Message externalization/internationalization
*/
#Bean
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("Messages");
return messageSource;
}
#Bean
public SpringResourceTemplateResolver templateResolver(){
// SpringResourceTemplateResolver automatically integrates with Spring's own
// resource resolution infrastructure, which is highly recommended.
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(this.applicationContext);
templateResolver.setPrefix("/WEB-INF/views/");
templateResolver.setSuffix(".html");
templateResolver.setCharacterEncoding("UTF-8");
// HTML is the default value, added here for the sake of clarity.
templateResolver.setTemplateMode(TemplateMode.HTML);
// Template cache is true by default. Set to false if you want
// templates to be automatically updated when modified.
templateResolver.setCacheable(true);
return templateResolver;
}
#Bean
public SpringTemplateEngine templateEngine(){
// SpringTemplateEngine automatically applies SpringStandardDialect and
// enables Spring's own MessageSource message resolution mechanisms.
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
// Enabling the SpringEL compiler with Spring 4.2.4 or newer can
// speed up execution in most scenarios, but might be incompatible
// with specific cases when expressions in one template are reused
// across different data types, so this flag is "false" by default
// for safer backwards compatibility.
templateEngine.setEnableSpringELCompiler(true);
return templateEngine;
}
#Override
public void configureViewResolvers(ViewResolverRegistry registry) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine());
resolver.setCharacterEncoding("UTF-8");
registry.viewResolver(resolver);
}
#Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.postgresql.Driver");
dataSource.setUrl("jdbc:postgresql://localhost:5432/postgres");
dataSource.setUsername("over");
// dataSource.setPassword("postgres"); Можно установить пароль для базы данных.
return dataSource;
}
#Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource());
}
}
DispatcherSerlvet config
public class MySpringMvcDispatcherSerlvetIntitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
#Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
/* Класс знает теперь где находится spring конфигурация */
#Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{SpringConfig.class};
}
/* Все http запросы от пользователя посылаем на dispatcher servlet */
#Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
#Override
public void onStartup(ServletContext aServletContext) throws ServletException {
super.onStartup(aServletContext);
registerCharacterEncodingFilter(aServletContext);
registerHiddenFieldFilter(aServletContext);
}
private void registerHiddenFieldFilter(ServletContext aContext) {
aContext.addFilter("hiddenHttpMethodFilter",
new HiddenHttpMethodFilter()).addMappingForUrlPatterns(null ,true, "/*");
}
private void registerCharacterEncodingFilter(ServletContext aContext) {
EnumSet<DispatcherType> dispatcherTypes = EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD);
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
characterEncodingFilter.setEncoding("UTF-8");
characterEncodingFilter.setForceEncoding(true);
FilterRegistration.Dynamic characterEncoding = aContext.addFilter("characterEncoding", characterEncodingFilter);
characterEncoding.addMappingForUrlPatterns(dispatcherTypes, true, "/*");
}
}
This is my exeption :
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.logException - Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supported]
Controller
#RequestMapping("/categories")
#Controller
public class CategoriesController {
private CategoriesDao categoriesDAO;
#Autowired
public void setCategoriesDAO(CategoriesDao categoriesDAO)
{
this.categoriesDAO = categoriesDAO;
}
#GetMapping
public String index(Model model) {
model.addAttribute("category", new Category());
model.addAttribute("categories", categoriesDAO.index());
return "categories/index";
}
#PostMapping
public String addCategory(#ModelAttribute("category") #Valid Category category,
BindingResult bindingResult, Model model) {
if(bindingResult.hasErrors()) {
model.addAttribute("categories", categoriesDAO.index());
return "categories/index";
}
categoriesDAO.addCategory(category);
return "redirect:categories";
}
#DeleteMapping("/{id}")
public String deleteCategory(#PathVariable("id") int id) {
categoriesDAO.deleteCategory(id);
return "redirect:/categories";
}
#GetMapping("/{id}/edit")
public String editCategory(Model model, #PathVariable("id") int id) {
model.addAttribute("editCategory", categoriesDAO.editCategory(id));
return "categories/edit";
}
#PatchMapping("/{id}")
public String updateCategory(#ModelAttribute("editCategory") Category updateCategory,
#PathVariable("id") int id) {
categoriesDAO.updateCategory(id, updateCategory);
return "redirect:{id}/edit";
}
}
To implement the configure method in the main class you could do:
#SpringBootApplication
public class Main extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
#Override
protected SpringApplicationBuilder configure(
SpringApplicationBuilder builder) {
return builder.sources(Main.class);
}
}
I think these dependencies should be enough (assuming you are using Thymeleaf):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.3</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
EDIT: Your ModelAttribute 'Category' might not be parsed successfully. Either the mapping fails, or maybe the validation? You might find out by adding an error handler and/or debugging the sessions.
Other ideas: Use #RestController instead of #Controller. Try to make the request mappings more explicit. Instead of on the class, put one on each method:
#RequestMapping(value = "/categories", produces = "application/json", method=RequestMethod.GET)
public String index(Model model) {
...
#RequestMapping(value = "/categories", produces = "application/json", method=RequestMethod.POST)
public String addCategory(#ModelAttribute("category") #Valid Category category,
BindingResult bindingResult, Model model) {
...
Thanks everyone! I found answer on my question. It was some problems with HiddenHttpMethodFilter. I just added this code in my Spring Config and extra dependencies was . It stared to work!!!
public FilterRegistrationBean<HiddenHttpMethodFilter> hiddenHttpMethodFilter() {
FilterRegistrationBean<HiddenHttpMethodFilter> filterRegistrationBean =
new FilterRegistrationBean<HiddenHttpMethodFilter>(new HiddenHttpMethodFilter());
filterRegistrationBean.setUrlPatterns(Collections.singletonList("/*"));
return filterRegistrationBean;
}
I have built a Spring-Boot application that works with jwt authentication.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.diplie</groupId>
<artifactId>rest-api</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.0.RC1</version>
</parent>
<properties>
<springfox-version>2.2.2</springfox-version>
<java.version>1.8</java.version>
<maven.test.skip>true</maven.test.skip>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>LATEST</version>
</dependency>
<!-- Swagger 2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox-version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox-version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>repository.springsource.milestone</id>
<name>SpringSource Milestone Repository</name>
<url>http://repo.springsource.org/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>repository.springsource.milestone</id>
<name>SpringSource Milestone Repository</name>
<url>http://repo.springsource.org/milestone</url>
</pluginRepository>
</pluginRepositories>
</project>
I want have a basic authentication, when I use Swagger I want to have a popup with when I click on the Try Out button
For example:
how can use two security(form base,JWT token) filters of spring security on same endpoint?
WebSecurityConfig
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().and().anonymous().and().servletApi().and().authorizeRequests()
// Allow anonymous resource requests
.antMatchers("/swagger-ui.html").permitAll().antMatchers("/").permitAll()
.antMatchers("/webjars/springfox-swagger-ui/**").permitAll().antMatchers("/swagger-resources/**")
.permitAll().antMatchers("/v2/api-docs").permitAll().antMatchers("/favicon.ico").permitAll()
.antMatchers("**/*.html").permitAll().antMatchers("**/*.css").permitAll().antMatchers("**/*.js")
.permitAll()
// Allow anonymous logins
.antMatchers("/user/User").permitAll().antMatchers("/locality/**").hasAuthority("Admin")
.antMatchers("/category/**").hasAuthority("Admin").antMatchers("/item").hasAuthority("Item")
.antMatchers("/item/userItems").hasAuthority("Item").antMatchers("item/lookFor").permitAll()
.antMatchers("item/items").hasAuthority("User")
// All other request need to be authenticated
.anyRequest().authenticated().and()
// And filter other requests to check the presence of JWT in
// header
.addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// Créer un compte par défaut
auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN");
}
}
TokenAuthenticationService
public class TokenAuthenticationService {
static ResourceBundle bundle = ResourceBundle.getBundle("application");
static void addAuthentication(HttpServletResponse res, String username) {
String JWT = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + getExpirationTime()))
.signWith(SignatureAlgorithm.HS512, getSecret()).compact();
res.addHeader(getHeaderString(), getTokenPrefix() + " " + JWT);
}
static Authentication getAuthentication(HttpServletRequest request) {
String token = request.getHeader(getHeaderString());
if (token != null) {
// Analyse du jeton.
String user = Jwts.parser().setSigningKey(getSecret()).parseClaimsJws(token.replace(getTokenPrefix(), ""))
.getBody().getSubject();
return user != null ? new UsernamePasswordAuthenticationToken(user, null, emptyList()) : null;
}
return null;
}
/**
* #return the secret
*/
public static String getSecret() {
return bundle.getString("secret");
}
/**
* #return the expirationTime
*/
public static long getExpirationTime() {
return Long.valueOf(bundle.getString("expiration.time"));
}
/**
* #return the tokenPrefix
*/
public static String getTokenPrefix() {
return bundle.getString("token.prefix");
}
/**
* #return the headerString
*/
public static String getHeaderString() {
return bundle.getString("header.string");
}
}
JWTLoginFilter
public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {
public JWTLoginFilter(String url, AuthenticationManager authManager) {
super(new AntPathRequestMatcher(url));
setAuthenticationManager(authManager);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException, IOException, ServletException {
AccountCredentials creds = new ObjectMapper().readValue(req.getInputStream(), AccountCredentials.class);
return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(creds.getUsername(),
creds.getPassword(), Collections.emptyList()));
}
#Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException, ServletException {
TokenAuthenticationService.addAuthentication(res, auth.getName());
}
}
JWTAuthenticationFilter
public class JWTAuthenticationFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
Authentication authentication = TokenAuthenticationService.getAuthentication((HttpServletRequest) request);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
AccountCredentials
public class AccountCredentials {
private String username;
private String password;
/**
*
*/
public AccountCredentials() {
super();
}
/**
* #return the username
*/
public String getUsername() {
return username;
}
/**
* #param username
* the username to set
*/
public void setUsername(String username) {
this.username = username;
}
/**
* #return the password
*/
public String getPassword() {
return password;
}
/**
* #param password
* the password to set
*/
public void setPassword(String password) {
this.password = password;
}
}
You will have to create two different WebSecurityConfigurerAdapter configurations with different root URLs. If the URLs overlap (ie /admin and /**) then you will need to define priority by using #Order annotation on the configuration.
Here's a working example for HTTP Basic and Form based authentication.
https://github.com/ConsciousObserver/TestMultipleLoginPagesFormAndBasic.git
package com.test;
import javax.servlet.http.HttpSession;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
#SpringBootApplication
public class TestMultipleLoginPagesApplication {
public static void main(String[] args) {
SpringApplication.run(TestMultipleLoginPagesApplication.class, args);
}
}
#Controller
class MvcController {
#RequestMapping(path="form/formLogin", method=RequestMethod.GET)
public String formLoginPage() {
return "formLogin";
}
#RequestMapping(path="form/formHome", method=RequestMethod.GET)
public String formHomePage() {
return "formHome";
}
#RequestMapping(path="basic/basicHome", method=RequestMethod.GET)
public String userHomePage() {
return "basicHome";
}
#RequestMapping(path="basic/logout", method=RequestMethod.GET)
public String userLogout(HttpSession session) {
session.invalidate();
return "basicLogout";
}
}
#Configuration
#Order(1)
class FormSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/form/**")
.authorizeRequests()
.anyRequest().hasRole("FORM_USER")
.and()
.formLogin()
.loginPage("/form/formLogin").permitAll()
.loginProcessingUrl("/form/formLoginPost").permitAll()
.defaultSuccessUrl("/form/formHome")
.and()
.logout().logoutUrl("/form/logout").logoutSuccessUrl("/form/formLogin")
.and()
.httpBasic().disable()
.csrf().disable();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password("test")
.roles("FORM_USER");
}
}
#Configuration
#Order(2)
class BasicAuthSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/basic/**")
.authorizeRequests()
.anyRequest().hasRole("BASIC_USER")
.antMatchers("/basic/logout").permitAll()
.and()
.httpBasic()
.and()
.csrf().disable();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("basic_user")
.password("test")
.roles("BASIC_USER");
}
}
#Configuration
#Order(3)
class RootUrlSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
/*
* Put any security expectations from the root URL here, currently everything is permitted.
* Since it's the last in the order /form/** and /basic/** have a priority over it.
*/
http.antMatcher("/**")
.authorizeRequests()
.anyRequest().permitAll();
}
}
Note: Since these login pages are not from different applications, they share the SecurityContextHolder or the security context. So if you login from one login page and then try to go the protected resource of the other, you won't be redirected to the next login page. Instead you'll get the 403 (depending on the roles assigned by the different login pages). At a time only one login session can be maintained.
I am building rest API using Spring Boot v1.3.3. API is secured by Spring Security. I have implemented custom user details service to have custom principal in authentication context.
I needed to share sessions of API with other Spring app so I choosen to implement Spring Session with Redis server in my app using this tutorial docs.spring.io/spring-session/docs/current/reference/html5/guides/security.html. Unfortunetly it caused Authentication Principal to stop working. When I am trying to get current Principal either by annotation #AuthenticationPrincipal CustomUserDetails user or by SecurityContextHolder.getContext().getAuthentication().getPrincipal() it returns my custom user details but with Id = 0 and all fields set to null
(screen from debugging). I can't even get username from SecurityContextHolder.getContext().getAuthentication().getName().
After I commented Redis code and maven dependency it works (see debug screen). How to make it working with Spring Session and Redis server?
Here is some code from the app:
Some example method to check Principal
#RequestMapping(value = "/status", method = RequestMethod.GET)
public StatusData status(#AuthenticationPrincipal CustomUserDetails user) {
User user2 = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (user != null) {
String name = user.getUsername();
return new StatusData(name);
} else return new StatusData(null);
}
Application and Redis config:
#Configuration
#EnableRedisHttpSession
public class AppConfig {
#Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
#Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
return serializer;
}
#Bean
public ShaPasswordEncoder shaEncoder() {
return new ShaPasswordEncoder(256);
}
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean(name = "messageSource")
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
resourceBundleMessageSource.setBasename("messages/messages");
return resourceBundleMessageSource;
}
#Bean
public Validator basicValidator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(messageSource());
return validator;
}
public AppConfig() {
DateTimeZone.setDefault(DateTimeZone.UTC);
}
}
Initializer (used for Redis Session)
public class Initializer extends AbstractHttpSessionApplicationInitializer {
}
SecurityInitializer (used for Redis session)
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
public SecurityInitializer() {
super(WebSecurityConfig.class, AppConfig.class);
}
}
WebSecurityConfig (Spring Security config)
#Configuration
#EnableWebSecurity
//#EnableWebMvcSecurity
#ComponentScan(basePackageClasses = {UserRepository.class, CustomUserDetailsService.class})
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private DataSource dataSource;
#Autowired
private UserDetailsService customUserDetailsService;
#Autowired
private HttpAuthenticationEntryPoint httpAuthenticationEntryPoint;
#Autowired
private AuthSuccessHandler authSuccessHandler;
#Autowired
private AuthFailureHandler authFailureHandler;
#Autowired
private HttpLogoutSuccessHandler logoutSuccessHandler;
#Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
/**
* Persistent token repository stored in database. Used for remember me feature.
*/
#Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setDataSource(dataSource);
return db;
}
/**
* Enable always remember feature.
*/
#Bean
public AbstractRememberMeServices rememberMeServices() {
CustomTokenPersistentRememberMeServices rememberMeServices = new CustomTokenPersistentRememberMeServices("xxx", customUserDetailsService, tokenRepository());
rememberMeServices.setAlwaysRemember(true);
rememberMeServices.setTokenValiditySeconds(1209600);
return rememberMeServices;
}
/**
* Configure spring security to use in REST API.
* Set handlers to immediately return HTTP status codes.
* Enable remember me tokens.
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(httpAuthenticationEntryPoint)
.and()
.authorizeRequests()
.antMatchers("/cookie", "/register", "/redirect/**", "/track/**")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.successHandler(authSuccessHandler)
.failureHandler(authFailureHandler)
.and()
.logout()
.permitAll().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)
.and()
.rememberMe().rememberMeServices(rememberMeServices())
.and()
.headers()
.addHeaderWriter(new HeaderWriter() {
/**
* Header to allow access from javascript AJAX in chrome extension.
*/
#Override
public void writeHeaders(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
String corsUrl = "https://mail.google.com";
if (httpServletRequest.getHeader("Origin") != null && httpServletRequest.getHeader("Origin").equals(corsUrl)) {
httpServletResponse.setHeader("Access-Control-Allow-Origin", "https://mail.google.com");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-Control-Expose-Headers", "Location");
}
}
});
}
/**
* Set custom user details service to allow for store custom user details and set password encoder to BCrypt.
*/
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(customUserDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
}
Maven dependencies
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>models</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.3.Final</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>org.jadira.usertype</groupId>
<artifactId>usertype.core</artifactId>
<version>3.1.0.CR1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
</dependency>
<dependency>
<groupId>com.maxmind.geoip2</groupId>
<artifactId>geoip2</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>com.ganyo</groupId>
<artifactId>gcm-server</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.0.4.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
I solved this problem. It turned out that Spring-Session serializes the Principal object. My custom implementation of UserDetails was subclass of Hibernate Model User class. I solved it by implementing Serializable interface in my custom UserDetails, User model and all classes used in this model.
To make it work in my case I had as well to make sure the Servlet filters were set up in the right order.
For me that was:
...
<filter-name>CharacterEncodingFilter</filter-name>
...
<filter-name>springSessionRepositoryFilter</filter-name>
...
<filter-name>springSecurityFilterChain</filter-name>
...
<filter-name>csrfFilter</filter-name>
...
After that, the principal was not empty anymore.
As #yglodt said, the problem is the filter's order in the spring security filter chain.
In Java Config way, just set an higher precedence to Redis configuration class
#Configuration
#EnableRedisHttpSession
#Order(Ordered.HIGHEST_PRECEDENCE)
public class RedisConfig extends AbstractHttpSessionApplicationInitializer {
#Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
}
I set the highest precedence, but maybe something lower is enough.
Now the principal should be correctly populated.
The order of the HttpSecurity chain is important:
Does not work, and leaves principal name null:
.authorizeRequests()
.antMatchers("/api/register").permitAll()
.anyRequest().authenticated()
Works correct:
.authorizeRequests()
.anyRequest().authenticated()
.antMatchers("/api/register").permitAll()
EDIT: 2022 This answer is outdated and will throw an IllegalStateException according to #BendaThierry.com