I made a SpringBoot application and now I'm at the login part.
I've made some custom restrictions with a custom annotation.
The problem is that this custom annotation is applied to the user after the password has been ecncripted.
This is the PasswordConstraint
public class PasswordConstraintValidator implements ConstraintValidator<ValidPassword, User>{
#Override
public void initialize(final ValidPassword arg0){}
#SneakyThrows
#Override
public boolean isValid(User user, ConstraintValidatorContext context) {
Properties props = new Properties();
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("passay.properties");
props.load(inputStream);
MessageResolver resolver = new PropertiesMessageResolver(props);
PasswordValidator validator = new PasswordValidator(resolver, Arrays.asList(
new LengthRule(8, 16),
new CharacterRule(EnglishCharacterData.UpperCase, 1),
new CharacterRule(EnglishCharacterData.LowerCase, 1),
new CharacterRule(EnglishCharacterData.Digit, 1),
new CharacterRule(EnglishCharacterData.Special, 1),
new WhitespaceRule(),
new IllegalSequenceRule(EnglishSequenceData.Alphabetical, 5, false),
new IllegalSequenceRule(EnglishSequenceData.Numerical, 5, false)
));
RuleResult result = validator.validate(new PasswordData(user.getPasswordHash()));
if (result.isValid()) {
return true;
}
List<String> messages = validator.getMessages(result);
String messageTemplate = String.join(",", messages);
context.buildConstraintViolationWithTemplate(messageTemplate)
.addConstraintViolation()
.disableDefaultConstraintViolation();
return false;
}
}
This is where I encrypt the password
#Qualifier("getPasswordEncoder")
#Autowired
private PasswordEncoder passwordEncoder;
public void registerNewUserAccount(User user){
Optional<User> userOptional= userRepository.findUserByEmailAddress(user.getEmailAddress());
if(userOptional.isPresent()){
throw new IllegalStateException("email taken!");
}
// Se comenteaza pentru ca: Validarea parolei se face pe hashPassword
//Dupa rezolvarea problemei, se decomenteaza
user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash()));
user.setEnabled(false);
Optional<User> saved = Optional.of(user);
saved.ifPresent(u -> {
try {
String token = UUID.randomUUID().toString();
verificationTokenService.save(user, token);
try {
emailService.sendHtmlMail(u);
} catch (MessagingException e) {
e.printStackTrace();
}
} catch (Exception e){
e.printStackTrace();
}
});
userRepository.save(user);
System.out.println(user);
saved.get();
}
Here is the user:
#Entity
#Table
#ValidPassword
public class User implements UserDetails {
#Id
#SequenceGenerator( //se auto-incrementeaza pkul ?
name = "user_sequence",
sequenceName = "user_sequence",
allocationSize = 1
)
#GeneratedValue( ///??????????
strategy = GenerationType.SEQUENCE,//maybe auto if not working
generator = "user_sequence"
)
//TODO Change camelCase to python_format
private Long id;
private String userName;
#NonNull
#NotBlank(message = "New password is mandatory")
private String passwordHash;
private String firstName;
private String lastName;
private String phoneNumber;
#Email
private String emailAddress;
public String address1;
public String address2;
private String city;
private String country;
private String zipcode;
private boolean enabled;
The issue here is that when you save an entity to the database, the validation annotations are also checked, which will then trigger the ConstraintViolationException.
So the best approach is to follow the Data Transfer Object pattern. I suggest you create a new class say UserDto which will be similar or even a replica of your User entity, ideally your UserDto class should only contain the field that the controller needs inclusive of all the necessary validation annotations.
In the service class where you encrypted your password, before saving the user, convert the UserDto instance to a User entity but remember to remove the #ValidPassword annotation from the User entity since you don't want to validate the encrypted password and you've already checked for the validations from your UserDto class
Remember: UserDto should not be an entity class since we don't want it in our database, we only need it for validation purpose.
Related
I have a Spring Boot Rest API. I want to create users and set a unique constraint on their email and username. That works well so far. Here are the main classes and methods:
#Entity
#NoArgsConstructor
#Getter
#Setter
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(unique = true)
#NotNull
private String email;
#Column(unique = true)
#NotNull
private String username;
#NotNull
private String password;
public User(String email, String username, String password) {
this.email = email;
this.password = password;
this.username = username;
}
}
#Data
#NoArgsConstructor
#AllArgsConstructor
public class SignupRequest {
#NotNull
private String email;
#NotNull
private String username;
#NotNull
private String password;
}
#CrossOrigin(value = "*")
#PostMapping("/signup")
public ResponseEntity<?> signup(#Valid #RequestBody SignupRequest signupRequest) {
signupService.signup(signupRequest);
return ResponseEntity.ok().build();
}
#Service
public class SignupServiceImpl implements SignupService {
#Override
public void signup(SignupRequest signupRequest) throws MessagingException, UnsupportedEncodingException {
User user = new User();
User user = new User(signupRequest.getEmail(), signupRequest.getUsername(), signupRequest.getPassword());
user = userRepository.save(user);
}
}
#Repository
#Component
public interface UserRepository extends CrudRepository<User, Long> {}
Now, the thing is, when I send a POST request to that endpoint with a username or email that already exists, I just get the http response 500 Internal Server Error. But I want to return a different status code and some Error message indicating that the email/username already exists.
Now two questions:
How can I modify the response globally? I could surround the userRepository.save(user) method with a try catch block, but I would have to do that in all the methods where I save a user separately. Can I define something like that globally?
The userRepository.save(user) method just returns a. JdbcSQLIntegrityConstraintViolationException with a pretty verbose message. Is there a way to clearly determine WHAT exactly went wrong (unique username constraint failed, unique email constraint failed, ...)? I could check if a user with that username or email exists by writing a method in the userRepository, but that looks like a lot of unnecessary sql queries to me. Is there a better way?
To answer your first question, You can handle exception globally via spring exception handling mechanism. You could use spring ControllerAdvice. Here you can set generic error response and custom http code. Here is an example of ControllerAdvice
#ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler
{
#ExceptionHandler(UserNotFoundException.class)
public final ResponseEntity<ErrorResponse> handleUserNotFoundException(UserNotFoundException ex, WebRequest request) {
String details = ex.getLocalizedMessage();
ErrorResponse error = new ErrorResponse(ApplicationConstants.RECORD_NOT_FOUND, details);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
#ExceptionHandler(Exception.class)
public final ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex, WebRequest request) {
String details = ex.getLocalizedMessage();
ErrorResponse error = new ErrorResponse(ApplicationConstants.SERVER_ERROR, details);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
public class ErrorResponse
{
public ErrorResponse(String message, String details) {
super();
this.message = message;
this.details = details;
}
private String message;
private String details;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
}
Now about second question you can loop through all the cause and check unique constraint name to find out what exception violated. But better approach would be to check first and if found then throw error.
In a Rest appplication developped with Spring, I use POJO classes, DTO and entity for users management. Here is an abstract of my entity class.
#Entity
#Table(name="users")
#Getter #Setter
#AllArgsConstructor #NoArgsConstructor
public class UserEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, unique = true)
private String userKeyId;
#Column(nullable = false, length = 50)
private String firstName;
#Column(nullable = false, length = 50)
private String lastName;
#Column(nullable = false, length = 120, unique = true)
private String email;
#Column(nullable = false)
private String encryptedPassword;
#Column
private String emailVerificationToken;
#Column(name = "email_verification_status", columnDefinition = "BOOLEAN NOT NULL DEFAULT FALSE")
private Boolean emailVerificationStatus = false;
#Column(name="is_account_non_expired")
private Boolean isAccountNonExpired;
#Column(name="is_account_non_locked")
private Boolean isAccountNonLocked;
#Column(name="is_credentials_non_expired")
private Boolean isCredentialsNonExpired;
#Column(name="is_enabled")
private Boolean isEnabled;
#Column(name="is_logged_in")
private Boolean isLoggedIn;
#ManyToMany(cascade= { CascadeType.PERSIST }, fetch = FetchType.EAGER )
#JoinTable(
name = "user_role",
joinColumns = #JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns=#JoinColumn(name = "role_id", referencedColumnName = "id"))
private Collection<RoleEntity> roles;
#CreationTimestamp
#Temporal(TemporalType.DATE)
#Column(name="created_at")
private Date createdAt;
#UpdateTimestamp
#Temporal(TemporalType.DATE)
#Column(name="updated_at")
private Date updatedAt;
}
I have a UserServiceImpl class that implements UserDetails
I do have then to implement loadUserByUsername
#Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
UserEntity userEntity = userRepository.findByEmail(email);
if( userEntity == null) {
throw new UsernameNotFoundException("User email is not in the database");
} else {
validateLoginAttempt(userEntity);
log.info("Returning User : " + userEntity.getFirstName() + " " + userEntity.getLastName());
userEntity.setLastLoginDateDisplay(userEntity.getLastLoginDate());
userEntity.setLastLoginDate(new Date());
userRepository.save(userEntity);
return new UserPrincipal(userEntity);
}
}
If user exists I call a method to validate authentication.
private void validateLoginAttempt(UserEntity user) {
if(user.getIsAccountNonLocked()) {
if(loginAttemptService.hasExceededMaxAttempts(user.getEmail())) {
user.setIsAccountNonLocked(Boolean.FALSE);
} else {
user.setIsAccountNonLocked(Boolean.TRUE);
}
} else {
loginAttemptService.evictUserFromLoginAttemptCache(user.getEmail());
}
}
This method allows me to check if the user account is locked or not and if user tried to connect too many times.
My LoginAttemptServiceImpl is the following:
#Service
public class LoginAttemptServiceImpl implements LoginAttemptService {
public static final int MAXIMUM_AUTH_ATTEMPT = 5;
public static final int AUTH_ATTEMPT_INCREMENT = 1;
private LoadingCache<String, Integer> loginAttemptCache;
private String username;
public LoginAttemptServiceImpl() {
super();
loginAttemptCache = CacheBuilder.newBuilder()
.expireAfterWrite(15, TimeUnit.MINUTES)
.maximumSize(10000)
.build(new CacheLoader<>() {
#Override
public Integer load(String key) {
return 0;
}
});
}
#Override
public void evictUserFromLoginAttemptCache(String username) {
loginAttemptCache.invalidate(username);
}
#Override
public void addUserToLoginAttemptCache(String username) {
int attempts = 0;
try {
attempts = AUTH_ATTEMPT_INCREMENT + loginAttemptCache.get(username);
loginAttemptCache.put(username, attempts);
} catch (ExecutionException e) {
e.printStackTrace();
}
}
#Override
public boolean hasExceededMaxAttempts(String username) {
try {
return loginAttemptCache.get(username) >= MAXIMUM_AUTH_ATTEMPT;
} catch (ExecutionException e) {
e.printStackTrace();
}
return false;
}
#Override
public int getLoginAttempts(String username) throws ExecutionException {
return loginAttemptCache.get(username);
}
}
I also implemented an event listener for authentication failure:
#Component
public class AuthenticationFailureListener {
private final LoginAttemptService loginAttemptService;
#Autowired
public AuthenticationFailureListener(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService;
}
#EventListener
public void onAuthenticationFailure(AuthenticationFailureBadCredentialsEvent event) {
Object principal = event.getAuthentication().getPrincipal();
if (principal instanceof String) {
String username = (String) event.getAuthentication().getPrincipal();
loginAttemptService.addUserToLoginAttemptCache(username);
}
}
}
And finally my AuthenticationFilter allows me to manage successful and unsuccessful response:
#Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
String userName = ((UserPrincipal)authResult.getPrincipal()).getUsername();
// built the token
String token = Jwts.builder()
.setSubject(userName)
.setExpiration(new Date(System.currentTimeMillis() + SecurityConstants.EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SecurityConstants.getTokenSecret())
.compact();
UserService userService = (UserService) SpringApplicationContext.getBean("userServiceImpl");
UserDto userDto = userService.getUser(userName);
response.addHeader(SecurityConstants.HEADER_STRING_USERID, userDto.getUserKeyId());
response.addHeader(SecurityConstants.HEADER_STRING, SecurityConstants.TOKEN_PREFIX + token);
}
#SneakyThrows
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
// super.unsuccessfulAuthentication(request, response, failed);
int attempts;
if(loginAttemptService.hasExceededMaxAttempts(this.username)) {
attempts = loginAttemptService.getLoginAttempts(this.username);
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Attempt number " + attempts + ": Account is locked for 15 minutes");
} else {
attempts = loginAttemptService.getLoginAttempts(this.username);
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Attempt number " + attempts + ": " + (SecurityConstants.MAX_AUTH_ATTEMPTS - attempts) + " - before account is blocked");
}
}
Authentication works when it's successful... My issue concerns failure and i have 3 issues:
I would like to return an object in case of failure. the response.sendError should do the job but it doesn't. I also tried to return a Json response : https://www.baeldung.com/servlet-json-response
I use Guava cache but I also update database at the same time by setting isAccountNonLocked to false. I'd like to set the value to True once the cache is cleared.
I do not update the count of attempt in unsuccessfulAuthentication method. My response is always : Attempt number 0: 5 - before account is blocked
Thanks for help and for reading the whole text!
Regarding issue number 1, you can use a similar approach as the one mentioned in the link you posted, but use response.getWriter().write(String) and Jackson's ObjectMapper, like this:
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ObjectMapper mapper = new ObjectMapper();
response.getWriter().write(mapper.writeValueAsString( /*Your custom POJO here */ ));
For issue 2: I found a trick that solves it. Instead of updating the database at the same time i clear the cache, I make the update at login validation...
private void validateLoginAttempt(UserEntity user) {
if(user.getIsAccountNonLocked()) {
if(loginAttemptService.hasExceededMaxAttempts(user.getEmail())) {
user.setIsAccountNonLocked(Boolean.FALSE);
} else {
user.setIsAccountNonLocked(Boolean.TRUE);
}
} else {
if(!loginAttemptService.hasExceededMaxAttempts(user.getEmail())) {
user.setIsAccountNonLocked(Boolean.TRUE);
}
loginAttemptService.evictUserFromLoginAttemptCache(user.getEmail());
}
}
For issue 3:
In my WebSecurity class which extends WebSecurityConfigurerAdapter, I implemented a bean in order to inject it in my AuthenticationFilter.
Here is my bean:
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
Here is my AuthenticationFilter class. I initially added this class as component (bad idea which generated error messages).
// #Component
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final LoginAttemptService loginAttemptService;
private String username;
public AuthenticationFilter(AuthenticationManager authenticationManager, LoginAttemptService loginAttemptService) {
this.authenticationManager = authenticationManager;
this.loginAttemptService = loginAttemptService;
}
....
#Autowired
LessonService lsnService;
#PutMapping(path = "/{id}")
public ResponseEntity<Object> updateLesson(#PathVariable("id") Long id, #Valid #RequestBody LessonDto dto) {
try {
lsnService.findById(id);
dto.setId(id);
lsnService.save(dto);
return ResponseEntity.ok(dto);
}
catch (Exception e) {
ApiErrorMessage errorMessage = new ApiErrorMessage();
errorMessage.setStatusCode(400L);
errorMessage.setMessage(e.getMessage());
errorMessage.setDescription("The server cannot or will not process the request due to an apparent client error");
return ResponseEntity.badRequest().body(errorMessage);
}
}
Here's my problem. When I remove lsnService.findById(id);, update is working.
If I didn't add that code if a user update with unexisting ID, it will save another data.
Another problem is when I remove dto.setId(id);, both method from lsnService; findById(id); and save(dto); are working! But as you can see, repo must update the entity but it won't!!!
So, I tried to put #Transactional in saving. And I even try putting Thread.sleep(5000); 5 secs delay between those two services. Like this,
lsnService.findById(id);
Thread.sleep(5000);
dto.setId(id);
lsnService.save(dto);
But it doesn't work either!
#Autowired
private LessonJpaRepository repo;
#Override
public LessonDto findById(Long id) {
// TODO Auto-generated method stub
Lesson lesson = repo.getOne(id);
LessonDto dto = new LessonDto(lesson);
return dto;
}
#Override
public void save(LessonDto dto) {
// TODO Auto-generated method stub
repo.save(dto.getEntity());
System.out.println(dto.getId()+dto.getTitle()+dto.getStructure()+dto.getExplanation());
}
And then, I check output of that dto. It's all there! repo is not saving it! It's so strange to me. Got any ideas?
public class LessonDto {
private Long id;
#NotNull(message = "Title must not be null")
#NotBlank(message = "Title must not be blank")
#ValidLessonTitle(message = "Title must begin with uppercase character")
private String title;
#NotNull(message = "Structure must not be null")
#NotBlank(message = "Structure must not be blank")
private String structure;
#NotNull(message = "Explanation must not be null")
#NotBlank(message = "Explanation must not be blank")
private String explanation;
public LessonDto() {
}
public LessonDto(Lesson lesson) {
this.id=lesson.getId();
this.title=lesson.getTitle();
this.structure=lesson.getStructure();
this.explanation=lesson.getExplanation();
}
#java.beans.Transient
public Lesson getEntity() {
Lesson lesson = new Lesson();
lesson.setId(this.id);
lesson.setTitle(this.title);
lesson.setStructure(this.structure);
lesson.setExplanation(this.explanation);
return lesson;
}
//getters and setters
}
This is the entity
#Entity
public class Lesson implements Serializable {
/**
*
*/
private static final long serialVersionUID = 2239534946567783017L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "title")
private String title;
#Column(name = "structure")
private String structure;
#Column(name = "explanation")
private String explanation;
//getters and setters
}
There are 2 ways to make that work
Update instance that is retured by findById with values from DTO
Dont use findById as it fetches entity pointer (at least) to the cache and this might be the origin of problems. Try to use existsById instead
I am making a RESTFul API (not web-app) and adding Spring Security but unable to do it successfully.
After going through a lot of articles and posts here on stackoverflow, I am finally posting my question. Kindly go through it and let me know what I am missing or configuring wrongly?
Base Entity
#MappedSuperclass
#EntityListeners(AuditingEntityListener.class)
abstract class BaseEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "ID", nullable = false, updatable = false)
private Long ID;
#CreatedBy
#Column(name = "CreatedBy", nullable = false, updatable = false)
private String createdBy;
#CreatedDate
#Column(name = "CreatedDate", nullable = false, updatable = false)
private LocalDateTime createdDate;
#LastModifiedBy
#Column(name = "ModifiedBy")
private String modifiedBy;
#LastModifiedDate
#Column(name = "ModifiedDate")
private LocalDateTime modifiedDate;
...getters setters
}
Role Entity
#Entity
#Table(name = "ROLE")
public class Role extends BaseEntity {
#Column(name = "Name")
private String name;
...getters setters
}
User Entity
#Entity
#Table(name = "USER")
public class User extends BaseEntity {
#Column(name = "EmiratesID", unique = true, nullable = false, updatable = false)
private String emiratesID;
#Column(name = "FirstName")
private String firstName;
#Column(name = "LastName")
private String lastName;
#Column(name = "StaffID", unique = true, nullable = false, updatable = false)
private String staffID;
#Column(name = "Email", unique = true, nullable = false)
private String email;
#Column(name = "Password", nullable = false)
private String password;
#ManyToOne(optional = false, cascade = CascadeType.MERGE)
#JoinColumn(name = "ROLE_ID")
private Role role;
...getters setters
public UserDetails currentUserDetails() {
return CurrentUserDetails.create(this);
}
}
SecurtiyConfig Class
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final DataSource dataSource;
private final UserDetailsServiceImplementation userDetailsService;
#Autowired
public SecurityConfig(final DataSource dataSource, final UserDetailsServiceImplementation userDetailsService) {
this.dataSource = dataSource;
this.userDetailsService = userDetailsService;
}
#Bean
BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers("/console/**").permitAll()
.antMatchers("/", "/greetUser", "/register", "/login").permitAll()
.antMatchers("/user/**").hasAnyAuthority(ROLES.USER.getValue(), ROLES.ADMIN.getValue())
.antMatchers("/admin/**").hasAuthority(ROLES.ADMIN.getValue()).anyRequest().authenticated();
httpSecurity.csrf().disable();
// required to make H2 console work with Spring Security
httpSecurity.headers().frameOptions().disable();
}
#Autowired
public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Override
public void configure(WebSecurity webSecurity) {
webSecurity.ignoring().antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**");
}
CurrentUserDetails
public class CurrentUserDetails implements UserDetails {
private String ROLE_PREFIX = "ROLE_";
private Long userID;
private String emiratesID;
private String firstName;
private String lastName;
private String staffID;
private String email;
private String password;
private Role role;
public CurrentUserDetails(Long ID, String emiratesID, String firstName,
String lastName, String staffID, String email,
String password, Role role) {
super();
this.userID = ID;
this.emiratesID = emiratesID;
this.firstName = firstName;
this.lastName = lastName;
this.staffID = staffID;
this.email = email;
this.password = password;
this.role = role;
}
public Long getUserID() {
return userID;
}
public String getEmiratesID() {
return emiratesID;
}
public String getEmail() {
return this.email;
}
public Role getRole() {
return this.role;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> grantedAuthority = new ArrayList<>();
grantedAuthority.add(new SimpleGrantedAuthority(ROLE_PREFIX + role.getName()));
return grantedAuthority;
}
#Override
public String getPassword() {
return this.password;
}
#Override
public String getUsername() {
return this.email;
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
/**
* Helper method to add all details of Current User into Security User Object
* #param user User
* #return UserDetails
*/
public static UserDetails create(User user) {
return new CurrentUserDetails(user.getID(), user.getEmiratesID(),
user.getFirstName(), user.getLastName(),
user.getStaffID(), user.getEmail(),
user.getPassword(), user.getRole());
}
}
UserDetailsService
#Component/#Service
public class UserDetailsServiceImplementation implements UserDetailsService {
private static final Logger userDetailsServiceImplementationLogger = LogManager.getLogger(UserDetailsServiceImplementation.class);
private final UserRepository userRepository;
#Autowired
public UserDetailsServiceImplementation(final UserRepository userRepository) {
this.userRepository = userRepository;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (StringUtils.isEmpty(username)) {
userDetailsServiceImplementationLogger.error("UserDetailsServiceImplementation.loadUserByUsername() :: FAILED");
throw new UsernameNotFoundException("UserName is not passed");
}
User userFound = userRepository.findByEmail(username);
if (userFound == null) {
userDetailsServiceImplementationLogger.error("No user found with given username = {}", username);
throw new UsernameNotFoundException("No user found with given username");
}
return userFound.currentUserDetails();
}
}
UserController Class
#RestController
#RequestMapping(value = "/user")
public class UserController {
private static Logger userControllerLogger = LogManager.getLogger(UserController.class);
#Autowired
private PropertiesConfig propertiesConfig;
#Autowired
private UserManager userManager;
#RequestMapping(value = "/listAll", method = RequestMethod.GET)
public ResponseEntity<Map<String, Object>> getUsersList() {
userControllerLogger.info("UserController.getUsersList()[/listAll] :: method call ---- STARTS");
LinkedHashMap<String, Object> result = userManager.findAllUsers();
userControllerLogger.info("UserController.getUsersList()[/listAll] :: method call ---- ENDS");
return new ResponseEntity<>(result, HttpStatus.OK);
}
}
AdminContrller Class
#RestController
#RequestMapping(value = "/admin")
public class AdminController {
private static final Logger adminControllerLogger = LogManager.getLogger(AdminController.class);
private final PropertiesConfig propertiesConfig;
private final UserManager userManager;
#Autowired
public AdminController(final PropertiesConfig propertiesConfig, final UserManager userManager) {
this.propertiesConfig = propertiesConfig;
this.userManager = userManager;
}
#RequestMapping(value = "/home", method = {RequestMethod.GET})
public ResponseEntity<String> adminPortal(#RequestBody String adminName) {
adminControllerLogger.info("AdminController.adminPortal()[/home] :: method call ---- STARTS");
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UserDTO adminUser = userManager.findUserByEmail(auth.getName());
if (adminUser == null) {
throw new UsernameNotFoundException(propertiesConfig.getProperty(ApplicationProperties.Messages.NO_USER_FOUND.getValue()));
}
adminControllerLogger.info("AdminController.adminPortal()[/home] :: method call ---- ENDS");
return new ResponseEntity<>(ApplicationConstants.GeneralConstants.WELCOME.getValue() + adminUser.getStaffID(), HttpStatus.OK);
}
}
data.sql
Tried with both values ROLE_USER/ADMIN and USER/ADMIN
INSERT INTO ROLE(ID, CreatedBy, CreatedDate, ModifiedBy, ModifiedDate, Name) VALUES (-100, 'Muhammad Faisal Hyder', now(), '', null, 'ROLE_ADMIN'/'ADMIN')
INSERT INTO ROLE(ID, CreatedBy, CreatedDate, ModifiedBy, ModifiedDate, Name) VALUES (-101, 'Muhammad Faisal Hyder', now(), '', null, 'ROLE_USER'/'USER')
INSERT INTO USER(ID, CreatedBy, CreatedDate, ModifiedBy, ModifiedDate, EmiratesID, FirstName, LastName, Email, StaffID, Password, ROLE_ID) VALUES (-1, 'Muhammad Faisal Hyder', now(), '', null, 'ABCDEF12345', 'Muhammad Faisal', 'Hyder', 'faisal.hyder#gmail.com', 'S776781', '$2a$10$qr.SAgYewyCOh6gFGutaWOQcCYMFqSSpbVZo.oqsc428xpwoliu7C', -100)
INSERT INTO USER(ID, CreatedBy, CreatedDate, ModifiedBy, ModifiedDate, EmiratesID, FirstName, LastName, Email, StaffID, Password, ROLE_ID) VALUES (-2, 'Muhammad Faisal Hyder', now(), '', null, 'BCDEFG12345', 'John', 'Smith', 'John.Smith#gmail.com', 'S776741', '$2a$10$j9IjidIgwDfNGjNi8UhxAeLuoO8qgr/UH9W9.LmWJd/ohynhI7UJO', -101)
I have attached all possible classes I think which are necessary. Kindly let me know what can be the issue.
Articles I went through;
SO-1, SO-2, SO-3, SO-4, Article-1, Article-2
Resolved
#dur thanks to you for pointing it out and others as well for their helpful insights.
1- Use ROLE_ in db entries.
2- Once prefix is added in db then no need to explicitly add this in
#Override
public Collection<? extends GrantedAuthority> getAuthorities(){...}
3- .and().httpBasic(); was missing from SpringSecurity configuration.
4- This is very detailed, might be helpful to others as well.
The problem I'm seeing is that you're granting access for authority ADMIN but you're not adding this authority to the CurrentUserDetails, you're just adding their role. You should add the authority as well, i.e.
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> grantedAuthority = new ArrayList<>();
grantedAuthority.add(new SimpleGrantedAuthority(ROLE_PREFIX + role.getName()));
// add authority in addition to role (no role prefix)
grantedAuthority.add(new SimpleGrantedAuthority(role.getName()));
return grantedAuthority;
}
As #dur pointed out in comments, I am adding answer to my question.
1- Use ROLE_ in db entries.
2- Once prefix is added in db then no need to explicitly add this in
#Override
public Collection<? extends GrantedAuthority> getAuthorities(){...}
3- .and().httpBasic(); was missing from SpringSecurity configuration.
Since this post is very detailed, might be helpful to others as well. For corrected answer kindly refer to my git repo
I am a newbee in java and spring. My first exercise is project to simulate web phone book. I'm stuck with assigning contact to specific user, and later on displaying contacts for that specific user. Any idea or guideline is appreciated.
User class
#Entity
public class User implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private String lastName;
private String email;
private String username;
private String password;
#Transient
private String retypePassword;
#ManyToMany(fetch = FetchType.EAGER,cascade=CascadeType.ALL)
#JoinTable(name="users_roles",
joinColumns = {#JoinColumn(name="user_id", referencedColumnName="id")},
inverseJoinColumns = {#JoinColumn(name="role_id", referencedColumnName="roles_id")}
)
private List<Rolee> authorities;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "userForPhonebook")
#Fetch(value = FetchMode.SUBSELECT)
private Collection<Contact> allContacts;
// getters and setters
Contact class
#Entity
public class Contact implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int contactID;
private String name;
private String lastName;
private String email;
private String areaCode;
private String telNumber;
private String adress;
private String city;
private String note;
private String contactImage;
#JoinColumn(name = "users_id")
#ManyToOne(fetch = FetchType.EAGER, optional = false)
private User userForPhonebook;
// getters and setters
ContactServiceImpl class
#Service
public class ContactServiceImpl implements ContactService {
#Autowired
private ContactDAO contactDAO;
#Autowired
private UserDAO userDAO;
#Autowired
private ServletContext context;
#Override
public void addContact(ContactModel contactModel, MultipartFile[] contactImages, User user) {
Contact contact = new Contact();
/*
creating contact
*/
User user2 = userDAO.userGetById(user.getId()); //<-- i cant get user id
contact.setUserForPhonebook(user2);
// --------------------------------
User u = new User(); //
u.setId(2); // hard coding users id...
contact.setUserForPhonebook(u); // and its working fine
// rest of code to create contact ...
UserDAOImpl
// ...
#Override
public void addUser(User user) {
Session s = getCurrentSession();
Transaction trans = s.beginTransaction();
getCurrentSession().save(user);
trans.commit();
}
// ...
UserServiceImpl
// ...
#Override
public void addUser(UserModel userModel) {
User user = new User();
// creating user ...
getUserDAO().addUser(user);
}
// ...
RegisterController
// ...
#RequestMapping(value = "/addUser", method = RequestMethod.POST)
public String postRegister(#Valid #ModelAttribute("newUser") UserModel userModel, BindingResult results, ModelMap model) {
if (results.hasErrors()) {
return "addUser";
}
// checking if username, email exist in database ...
// password matching
getUserService().addUser(user);
return "index";
}
// ...
ContactController
// ...
#RequestMapping(value = "/addContact", method = RequestMethod.POST)
public String postAddContact(#Valid #ModelAttribute("addContact") ContactModel contactModel, BindingResult results, HttpServletRequest request, #RequestParam("contactImages") MultipartFile[] contactImages, User user)
throws FileNotFoundException, IOException {
if (results.hasErrors()) {
return "addContact";
}
getContactService().addContact(contactModel, contactImages, user);
return "redirect:phoneBook";
}
//...
You are not specifying how Spring is supposed to bind your User object in your postAddContact signature.
#RequestMapping(value = "/addContact", method = RequestMethod.POST)
public String postAddContact(
// Ok, this is a model retrieved from request params
#Valid #ModelAttribute("addContact") ContactModel contactModel,
// Ok, a BindingResult is mapped when the validation above occurs
BindingResult results,
// Ok, bind the internal HttlServletRequest
HttpServletRequest request,
// Ok, bind this to the multipart part of the request
#RequestParam("contactImages") MultipartFile[] contactImages,
// ... no idea how to bind this
User user)
throws FileNotFoundException, IOException {
// ...
}
You need to specify yourself which user is going to get the contact.
You could add the field inside your ContactModel object, like userId, and in your controller retrieve that user from database before adding the contact.
ContactController.java
// ...
#RequestMapping(value = "/addContact", method = RequestMethod.POST)
public String postAddContact(#Valid #ModelAttribute("addContact") ContactModel contactModel, BindingResult results, HttpServletRequest request, #RequestParam("contactImages") MultipartFile[] contactImages)
throws FileNotFoundException, IOException {
if (results.hasErrors()) {
return "addContact";
}
// Retrieve the user
User user = getUserService().retrieveUser(contactModel.getUserId());
getContactService().addContact(contactModel, contactImages, user);
return "redirect:phoneBook";
}
//...
You could also add a path variable, use the connected user, etc. Above code is just a suggestion.
The answer here helped me to assign currently active user to contact and my postAddContact looks like this. Sorry if I didn't correctly ask the question and I hope this will help someone else
#RequestMapping(value = "/addContact", method = RequestMethod.POST)
public String postAddContact(#Valid #ModelAttribute("addContact") ContactModel contactModel, BindingResult results, HttpServletRequest request, #RequestParam("contactImages") MultipartFile[] contactImages)
throws FileNotFoundException, IOException {
if (results.hasErrors()) {
return "addContact";
}
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
UserDetails userDetail = (UserDetails) auth.getPrincipal();
User u = userDAO.getUserByUsername(userDetail.getUsername());
request.getSession().setAttribute("id", u.getId());
int userId = (int) request.getSession().getAttribute("id");
User user = new User();
user.setId(userId);
getContactService().addContact(contactModel, contactImages, user);
return "redirect:phoneBook";
}