How should I encode automatically the subbmitted plain password field of my entity with Spring Data REST?
I'm using BCrypt encoder and I want to automatically encode the request's password field, when the client send it via POST, PUT and PATCH.
#Entity
public class User {
#NotNull
private String username;
#NotNull
private String passwordHash;
...
getters/setters/etc
...
}
First I tried to solve with #HandleBeforeCreate and #HandleBeforeSave event listeners but the User in it's argument is already merged, so I can't make any difference between the User's new password, or the old passwordHash:
#HandleBeforeSave
protected void onBeforeSave(User user) {
if (user.getPassword() != null) {
account.setPassword(passwordEncoder.encode(account.getPassword()));
}
super.onBeforeSave(account);
}
Is that possible, to use #Projection and SpEL on a setter method?
You can implement a Jackson JsonDeserializer:
public class BCryptPasswordDeserializer extends JsonDeserializer<String> {
public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
ObjectCodec oc = jsonParser.getCodec();
JsonNode node = oc.readTree(jsonParser);
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encodedPassword = encoder.encode(node.asText());
return encodedPassword;
}
}
And apply it to your JPA Entity property:
// The value of the password will always have a length of
// 60 thanks to BCrypt
#Size(min = 60, max = 60)
#Column(name="password", nullable = false, length = 60)
#JsonDeserialize(using = BCryptPasswordDeserializer.class )
private String password;
Modifying setter method of password field is sufficient, as shown below:
public void setPassword(String password) {
PasswordEncoder encoder = new BCryptPasswordEncoder();
this.password = encoder.encode(password);
}
Refer:
https://github.com/charybr/spring-data-rest-acl/blob/master/bookstore/src/main/java/sample/sdr/auth/bean/UserEntity.java
Some enhancement to #robgmills JsonDeserializer solution:
In Spring 5 introduce DelegatingPasswordEncoder. It is more flexible, see spring docs.
It is not nesessary to create PasswordEncoder every time at deserialization.
A big projects may has several JsonDeserializer's - better make them inner classes.
Usually encoding password hidden for get request. I've used #JsonProperty(access = JsonProperty.Access.WRITE_ONLY), see https://stackoverflow.com/a/12505165/548473
For Spring Boot code looks like:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static final PasswordEncoder PASSWORD_ENCODER = PasswordEncoderFactories.createDelegatingPasswordEncoder();
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(PASSWORD_ENCODER);
}
....
public class JsonDeserializers {
public static class PasswordDeserializer extends JsonDeserializer<String> {
public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
ObjectCodec oc = jsonParser.getCodec();
JsonNode node = oc.readTree(jsonParser);
String rawPassword = node.asText();
return WebSecurityConfig.PASSWORD_ENCODER.encode(rawPassword);
}
}
...
#Entity
public class User ...
#Column(name = "password")
#Size(max = 256)
#JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
#JsonDeserialize(using = JsonDeserializers.PasswordDeserializer.class)
private String password;
...
Related
I am trying to use spring-data-solr in order to access to my Solr instance through my Spring boot application. I have the following bean class:
#SolrDocument(solrCoreName = "associations")
public class Association implements PlusimpleEntityI {
#Id
#Indexed
private String id;
#Indexed
private String name;
#Indexed
private Point location;
#Indexed
private String description;
#Indexed
private Set<String> tags;
#Indexed
private Set<String> topics;
#Indexed
private Set<String> professionals;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Point getLocation() {
return location;
}
public void setLocation(Point location) {
this.location = location;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Set<String> getTags() {
return tags;
}
public void setTags(Set<String> tags) {
this.tags = tags;
}
public Set<String> getTopics() {
return topics;
}
public void setTopics(Set<String> topics) {
this.topics = topics;
}
public Set<String> getProfessionals() {
return professionals;
}
public void setProfessionals(Set<String> professionals) {
this.professionals = professionals;
}
}
I have implemented the following repository in order to access to the related information:
public interface AssociationsRepository extends SolrCrudRepository<Association, String> {
}
I have created a configuration class which looks like the following one:
#Configuration
#EnableSolrRepositories(basePackages = {"com.package.repositories"}, multicoreSupport = true)
public class SolrRepositoryConfig {
#Value("${solr.url}")
private String solrHost;
#Bean
public SolrConverter solrConverter() {
MappingSolrConverter solrConverter = new MappingSolrConverter(new SimpleSolrMappingContext());
solrConverter.setCustomConversions(new CustomConversions(null));
return solrConverter;
}
#Bean
public SolrClientFactory solrClientFactory () throws Exception {
return new MulticoreSolrClientFactory(solrClient());
}
#Bean
public SolrClient solrClient() throws Exception {
return new HttpSolrClient.Builder(solrHost).build();
}
#Bean
public SolrOperations associationsTemplate() throws Exception {
SolrTemplate solrTemplate = new SolrTemplate(solrClient());
solrTemplate.setSolrConverter(solrConverter());
return solrTemplate;
}
}
Unfortunately, when I try to read an association from my Solr instance I got the following error:
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [org.springframework.data.solr.core.geo.Point]
I don't understand why it is not able to find a converter if I have explicitly defined it in the solrTemplate() method.
This is my POM definition:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-solr</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
Thank you for your help.
EDIT:
I've also tried with different BUILD-RELEASEs but they are highly unstable and I've found a lot of errors using them.
Alessandro, as you can see directly in the GeoConverters class on GitHub, the implemented converters are only for:
org.springframework.data.geo.Point
and not for:
org.springframework.data.solr.core.geo.Point
Simply use this class and you don't even need a custom converter for this. Spring Data for Solr will perform the conversion for you.
I'm using a slightly patched version of the 3.0.0 M4, but I'm pretty sure this solution should apply seamlessly also to your case.
i met a problem in Studying with Spring data solr,this is my Configuration Class:
#Configuration
#EnableSolrRepositories(basePackages={"cn.likefund.solr.repository"}, multicoreSupport=true)
public class SolrContext {
static final String SOLR_HOST = "http://192.168.11.157:8080/solr";
#Bean
public SolrClient solrClient() {
return new HttpSolrClient(SOLR_HOST);
}
}
and this is my Repository:
package cn.likefund.solr.repository;
import java.util.List;
import org.springframework.data.solr.repository.SolrCrudRepository;
import cn.likefund.solr.model.Activity;
public interface ActivityRepository extends SolrCrudRepository<Activity, String>{
List<Activity> findByName(String name);
}
when I start the application,the message in console is this
error
When I delete the method findByName in the repository,the application start with no problem, i just want to the method findByName worked,anybody know what should i do with this problem?
here is the Activity Class:
#Entity
#SolrDocument(solrCoreName ="core_activity")
public class Activity implements Serializable{
private static final long serialVersionUID = 1566434582540525979L;
#Id
#Field(value = "id")
private String id;
#Field(value = "CREATEDT")
private String createdt;
#Indexed
#Field(value = "NAME")
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getCreatedt() {
return createdt;
}
public void setCreatedt(String createdt) {
this.createdt = createdt;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
So, obviously the CrudRepository is not created .
when you delete the findByName, can you manually query your repo ? (just to be sure the problem comes from the method, and not the SOLR schema)
have you tried to annotate annotate the method to explicitly set the query ? Something like
#Query("NAME:?0")
List findByName(String name);
I have created a web application based on Spring-Boot 1.2.2.RELEASE. I also have a database table where I manage user credentials. It has the basic columns e.g. id, username, and password. Also added salt column where I plan to store randomly generated salts per account.
I'm trying to use Spring Security and having some difficulty with authentication using the encoded password. So basically I have a Register Controller:
#Autowired
private UserRepository userRepository;
private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
#RequestMapping(value = "/register", method = RequestMethod.POST)
public ResponseEntity<User> register(#RequestParam(value = "email_address") String emailAddress, #RequestParam(value = "password") String password) {
String username = emailAddress;
String salt = KeyGenerators.string().generateKey();
User user = new User();
user.setUsername(emailAddress);
user.setEmailAddress(emailAddress);
user.setSalt(salt);
user.setPassword(passwordEncoder.encode(username+salt+password));
user.setCreatedBy(1);
user.setCreatedOn(new Date());
user.setLastUpdatedby(1);
user.setLastUpdatedOn(new Date());
user.setStartDate(new Date());
return new ResponseEntity<User>(this.userRepository.save(user), HttpStatus.OK);
}
Then I implemented UserDetailsService like this:
#Service
protected static class ApplicationUserDetailsService implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = this.userRepository.findByUsername(username);
if (user == null) {
return null;
}
List<GrantedAuthority> auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
String password = passwordEncoder.encode(username+user.getSalt()+user.getPassword());
return new org.springframework.security.core.userdetails.User(username, password, auth);
}
}
However, since BCryptPasswordEncoder generates new result everytime even if the input is the same, how will it be able to authenticate?
EDIT:
Just wanted to add that if I don't use any encoder and store the password as plain text, I am able to authenticate just fine. But of course that's not what I want to do.
UPDATE 1
Here's config for WebSecurityConfigurerAdapter. Inside it I have the configure method.
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableWebSecurity
protected static class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ApplicationUserDetailsService applicationUserDetailsService;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(applicationUserDetailsService).passwordEncoder(passwordEncoder);
}
#Bean
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
UPDATE 2
I got it working by extending DaoAuthenticationProvider. See updated code below.
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableWebSecurity
protected static class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ApplicationUserDetailsService applicationUserDetailsService;
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(applicationUserDetailsService);
authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
return authenticationProvider;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Bean
#Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
Also had to modify my RegisterController as shown below.
#RestController
public class RegisterController {
#Autowired
private UserRepository userRepository;
private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
#RequestMapping(value = "/register", method = RequestMethod.POST)
public ResponseEntity<User> register(#RequestParam(value = "email_address") String emailAddress, #RequestParam(value = "password") String password) {
User user = new User();
user.setUsername(emailAddress);
user.setEmailAddress(emailAddress);
user.setPassword(passwordEncoder.encode(password));
user.setSalt(KeyGenerators.string().generateKey());
user.setCreatedBy(1);
user.setCreatedOn(new Date());
user.setLastUpdatedby(1);
user.setLastUpdatedOn(new Date());
user.setStartDate(new Date());
return new ResponseEntity<User>(this.userRepository.save(user), HttpStatus.OK);
}
}
UPDATE 3
Forgot to include the updated UserDetailService implementation.
#Service
protected static class ApplicationUserDetailsService implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = this.userRepository.findByUsername(username);
if (user == null) {
return null;
}
List<GrantedAuthority> auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
String password = passwordEncoder.encode(user.getPassword());
return new org.springframework.security.core.userdetails.User(username, password, auth);
}
}
I might need to remove the salt column now since it's basically useless.
Hopefully this helps others as well.
My code like this:
Java:
#Autowired
private RedisTemplate<String,User> myTemplate;
#Override
public String login(String email, String password) {
User user = this.userRepository.findByEmailAndPassword(email, password);
System.out.println(user);
if (user == null) return null;
String key1 = "lic" + "$" + user.getId() + "$" + user.getRole() + "$" + user.getName() + "$" + user.getEmail();
ValueOperations<String, User> ops = this.myTemplate.opsForValue();
if (!this.myTemplate.hasKey(key1)) {
ops.set(key1, user);
}
return key1;
}
when app run, inject bean ,like this:
#SpringBootApplication
public class ApplicationApp extends WebMvcConfigurerAdapter {
// #Autowired
// private RedisTemplate<String,String> template;
#Bean
JedisConnectionFactory jedisConnectionFactory() {
return new JedisConnectionFactory();
}
#Bean
RedisTemplate<String, User> redisTemplate() {
final RedisTemplate<String, User> template = new RedisTemplate<String, User>();
template.setConnectionFactory(jedisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericToStringSerializer<User>(User.class));
template.setValueSerializer(new GenericToStringSerializer<User>(User.class));
return template;
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
// super.addInterceptors(registry);
registry.addInterceptor(new AuthorAccess()).addPathPatterns("/api/sc/**");
}
public static void main(String[] args) throws Exception {
SpringApplication.run(ApplicationApp.class, args);
}
}
then , call login service found error like this:
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type com.qycloud.oatos.license.domain.User to type java.lang.String
Set ValueSerializer with Jackson2JsonRedisSerializer instead of GenericToStringSerializer and it should work. GenericToStringSerializer do not support Object to String conversion.
template.setValueSerializer(new Jackson2JsonRedisSerializer<User>(User.class));
You need to register you're own implementation of TypeConverter or a ConversionService able to deal with User.class via eg. setTypeConverter(TypeConverter converter). Otherwise GenericToStringSerializer will just try using the DefaultConversionService which does not know about your type.
I'm using fasterXML's Jackson (v2.3.3) library to deserialize and serialize a custom class. The class is defined as following:
public class Person {
private String name;
private Map<String, Person> children;
// lots of other fields of different types with no issues
}
the keys of map children are the name fields. I receive data in JSON with each person object structured as following (I have omitted the other fields):
{"name":"Bob", "children":[{"name":"Jimmmy"},{"name":"Judy"}]}
(Many Fields such as children are optional and aren't serialized when null)
I have been storing children in a List<Person> so far with no issues, but many new use cases need to have access to the set of names or to a specific Person using his name as key. This is why I have decided to store them using a Map.
After some research, I think the best way is to use Annotations #JsonDeserialize and #JsonSerialize with a JsonDeserializer and JsonSerializer as parameter respectively for the field children:
public class Person {
private String id;
#JsonSerialize(using=MySerializer.class)
#JsonDeserialize(using=MyDeserializer.class)
private Map<String, Person> friends;
// lots of other fields
}
My question is: Does such a JsonSerializer/JsonDeserializer exist and if not, how do I define one?
edit: I have started implementing a custom Deserializer, but I get this exception:
com.fasterxml.jackson.databind.JsonMappingException: Class has no default (no arg) constructor
which is weird because I have defined a default constructor. Here is my custom Deserializer:
public class MyDeserializer extends JsonDeserializer<Map<String, Person>> {
public MyDeserializer() {
}
#Override
public Map<String, Person> deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode personsNodeArray = jp.getCodec().readTree(jp);
Map<String, Person> newChildren = null;
if (personsNodeArray.isArray() && personsNodeArray.size() > 0) {
newChildren = new HashMap<String, Person>();
for (JsonNode personNode : personsNodeArray) {
String id = personNode.get("name").asText();
// jsonMapper is a default ObjectMapper
newChildren.put(id, jsonMapper.readValue(personNode.toString(), Person.class));
}
}
return newChildren;
}
}
You can also consider reading children information as a collection of persons with subsequent conversion into a map. You can define a setter method (or a constructor parameter) to accept a List<Person> and then put each element into the Map<String, Person> children field. That would avoid unnecessary complexity of custom serialisation.
Here is an example:
public class JacksonChildren {
public static final String JSON = "{\"name\":\"Bob\", \"children\":[{\"name\":\"Jimmmy\"}," +
"{\"name\":\"Judy\"}]}";
public static class Person {
public String name;
private Map<String, Person> children = new HashMap<>();
public void setChildren(final List<Person> children) {
for (Person p : children) {
this.children.put(p.name, p);
}
}
#Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", children=" + children +
'}';
}
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.readValue(JSON, Person.class));
}
}
Output:
Person{name='Bob', children={Judy=Person{name='Judy', children={}}, Jimmmy=Person{name='Jimmmy', children={}}}}