I'm currently making an E-Commerce application and I'm trying to add "Add To Cart" functionality. I haven't yet worked with httprequests, so everyone has to login before accessing the website.
When a user logs in, goes onto the home page and tries to add a product to cart, it will send a post request to add that product.
#PostMapping("/cart/{id}")
public String addProduct(#PathVariable Long id, #ModelAttribute("cartItem") CartItems cartItem, Model model) {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
User user = ((User) principal);
Optional<Product> product = productService.findById(id);
cartItem.setProduct(product.get());
cartItem.setPrice(cartItem.getQuantity() * cartItem.getProduct().getPrice());
user.getCart().getCartItems().add(cartItem);
return "redirect:/cart";
}
User Class:
#Entity
#EntityListeners(AuditingEntityListener.class)
#Table(name = "users")
public class User implements Serializable, UserDetails
{
private static final long serialVersionUID = -8903772563656397127L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "user_id")
private Long id;
#Column(name = "username")
private String username;
#Column(name = "email")
private String email;
#Column(name = "password")
private String password;
#Column(name = "first_name")
private String firstName;
#Column(name = "last_name")
private String lastName;
#OneToOne(mappedBy = "user")
private Cart cart;
#Column(name = "address")
private String address;
USER DETAILS:
#Override
#Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
grantedAuthorities.add(new SimpleGrantedAuthority("UNDEFINED"));
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities);
}
The problem is casting the User class to Object principal from user details. I am getting the following error:
java.lang.ClassCastException: class org.springframework.security.core.userdetails.User cannot be cast to class com.nickjojo.ecomapp.entity.User (org.springframework.security.core.userdetails.User and com.nickjojo.ecomapp.entity.User are in unnamed module of loader 'app')
There's a couple of things I would improve here:
Your User class already implements UserDetails, so no need to convert it to a org.springframework.User in your UserDetailsService.loadUserByUsername. Simply return it.
Instead of going through the SecurityContext, you can then simply go with the #AuthenticationPrincipal annotation like so (note, it will be null if the user is not logged in):
#PostMapping("/cart/{id}")
public String addProduct(... #AuthenticationPrincipal user) {
// ...
}
I found the answer. I was casting User to the principle object, but instead I casted UserDetails to the object (they can be casted together), then got the object's username as specified as one of the fields when returning a UserDetails object in the UserDetailsService class. I then used that username to find the user through my service.
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = ((UserDetails) principal).getUsername();
User user = userService.findByUsername(username);
Related
My goal was to pass a List of Businesses to the model from the controller to display it in a view and I have succeeded, but have a bit of confusion.
When I initially tried using:
public User getCurrentAuthenticatedUser() {
UserDetailsImpl user = (UserDetailsImpl) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return user.getUser();
}
#GetMapping("")
public String list(Model model) {
model.addAttribute("businesses", userService.getCurrentAuthenticatedUser().getBusinesses());
return "business/list";
}
I got this error: "failed to lazily initialize a collection of role: com.xyz.User.businesses could not initialize proxy - no Session"
Then I tried:
#GetMapping("")
public String list(Model model) {
int userId = userService.getCurrentAuthenticatedUser().getId();
User user = userService.getById(userId); // gets User using Spring Data JPA UserRepository
List<Business> businesses = user.getBusinesses();
model.addAttribute("businesses", businesses);
return "business/list";
}
And this worked perfectly fine.
What was the issue using the first method. It seemed more simple rather than calling a User from the UserRepository. I've seen some posts that say you should use EAGER fetching, but that's just seems like a bandaid solution.
From the beginner's understanding: Since fetch type is LAZY the businesses don't exist yet in the User but are fetched on demand later on so there shouldn't be an issue.
Edit: After more thought I remembered that with basic Hibernate you would have to create Transactions and commit transactions. I'm assuming that User is not within a Transaction that's why I can't get businesses using the 1st method.
What would be a better solution to fetch the current Authenticated user? And that user's attributes such as a list of businesses.
Model Classes:
Business:
#Entity
#Table(name = "businesses")
public class Business {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private LocalDate date;
#ManyToOne(cascade={CascadeType.MERGE})
#JoinColumn(name="user_id")
private User user;
public Business() {
}
public Business(String name, String description, LocalDate date, User user) {
...
}
public Business(Long id, String name, String description, LocalDate date, User user) {
...
}
... getters/setters
}
USER:
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private boolean enabled;
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable( name = "users_roles",
joinColumns = #JoinColumn(name = "user_id"),
inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
#OneToMany(fetch = FetchType.LAZY, mappedBy="user", cascade={CascadeType.MERGE})
private List<Business> businesses;
... getters/setters
}
I have a spring boot project that has 3 types of users (Admin, Expert, Customer) and the application is for Experts that register on site for giving services like fixing computers to Customers that are asking help in site.
I have an inheritance of different kind of User types as following.
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "USER_TYPE", discriminatorType = DiscriminatorType.INTEGER)
public abstract class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private Set<String> roles = new HashSet<>();
// getter & setter...
}
#Entity
#DiscriminatorValue("1")
public class Admin extends User {
}
#Entity
#DiscriminatorValue("2")
public class Expert extends User {
private Byte[] expertPhoto;
private String password;
// some other fields & getter & setter...
}
#Entity
#DiscriminatorValue("3")
public class Customer extends User {
private Long credit;
private Set<CustomerOrder> orders = new HashSet<>();
// some other fields & getter & setter...
}
I want to use spring boot security and implement UserDetailsService, my problem is that how to design when I have different User types (Expert, Customer, etc.)?
I want users to be able to have different roles (admin, expert, customer) with one username.
How should I design my system to solve these requirements?
Your role modal seems a bit off. It is better to have a single type of User and fill it with list of a new Role entity. The new User entity will look like the following:
#Table(name = "user")
#Entity
public class User {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "username", unique = true, nullable = false)
private String username;
#Column(name = "password", nullable = false)
private String password;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(
name = "user_role",
joinColumns = {#JoinColumn(name = "user_id")},
inverseJoinColumns = {#JoinColumn(name = "role_id")},
)
private Set<Role> roles;
// getters and setters & other fields user can have
}
And the Role entity will look like this:
#Entity
#Table(name = "role")
public class Role {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "role_name", unique = true, nullable = false)
private String roleName;
#ManyToMany(mappedBy = "roles")
private Set<User> users;
}
Then, you need to implement org.springframework.security.core.userdetails.User interface to use as a concrete implementation of spring security class Useron your UserDetailsService. Notice that this class is also called User and is different than the User class on your system.
public class MyUserDetail extends User {
private String otherFieldsLikePhoto; // you can add different fields like this to keep extra information
public MyUserDetail(String username, String password, Collection<? extends GrantedAuthority> authorities, String otherFieldsLikePhoto) {
super(username, password, authorities);
this.otherFieldsLikePhoto = otherFieldsLikePhoto;
}
}
Then, you can create your UserDetailsService by implementing org.springframework.security.core.userdetails.UserDetailsService of spring security.
What you will achieve UserDetailsService is to load the user in the MyUserDetail format we just created. It will be something like this:
public class MyUserDetailsService implements UserDetailsService {
private final UserReadService userReadService; // put your service to get user from db
public MyUserDetailsService(UserReadService UserReadService) {
this.userReadService = UserReadService;
}
#Override
public UserDetails loadUserByUsername(String username) {
User user = userReadService.getByUsername(username); // get user from db
String otherFieldsLikePhoto = getUserPhotoOrAnythingElse(user); // get your extra fields however you want
return new MyUserDetail(
user.getUsername(),
user.getPassword(),
getAuthoritySetOfUser(user), // notice how we embed roles to UserDetail
otherFieldsLikePhoto
);
}
// this function is not necessary but useful to calculate authority set calculation on helper
private Set<SimpleGrantedAuthority> getAuthoritySetOfUser(User user) {
Set<Role> userRoles = user.getRoles(); // get roles of user like ADMIN, EXPERT etc.
Set<SimpleGrantedAuthority> authorities = roles.stream()
.map(rolex -> new SimpleGrantedAuthority(rolex.getRoleName()))
.collect(Collectors.toSet());
return authorities;
}
}
I created an entity class :
#Entity
#Table(name="users")
#Getter #Setter
public class UserModel implements Serializable {
#Setter(AccessLevel.NONE)
#Getter(AccessLevel.NONE)
private static final long serialVersionUID = -5608230793232883579L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(nullable = false, unique = true)
private String userId;
#Column(nullable = false, length = 50)
private String firstName;
#Column(nullable = false, length = 50)
private String lastName;
#Email
#Column(nullable = false, length = 120, unique = true)
private String email;
#Column(nullable = false)
private String encryptedPassword;
private Boolean emailVerificationStatus = false;
private String emailVerificationToken;
#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 List<RoleModel> roles;
#JsonManagedReference
#OneToMany(mappedBy = "user")
private List<ProjectModel> projects;
}
For the list of projects, I also have an entity class:
#Entity
#Table(name= "projects")
#Getter #Setter
public class ProjectModel implements Serializable {
#Setter(AccessLevel.NONE)
#Getter(AccessLevel.NONE)
public static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(nullable = false, unique = true)
private String projectId;
// ...
#Column
#JsonManagedReference
#OneToMany(mappedBy = "project")
private List<ObjectiveModel> objectives;
// ...
#JsonBackReference
#ManyToOne(
cascade = { CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH },
fetch = FetchType.LAZY
)
private UserModel user;
}
I also use a DTO layer to communicate with database:
#Getter #Setter
public class UserDto implements Serializable {
#Setter(AccessLevel.NONE)
#Getter(AccessLevel.NONE)
private static final long serialVersionUID = -5352357837541477260L;
// contains more information than models used for rest
private long id;
private String userId;
private String firstName;
private String lastName;
private String email;
private String password;
private String encryptedPassword;
private String emailVerificationToken;
private Boolean emailVerificationStatus = false;
private List<String> roles;
private List<ProjectDto> projects;
}
Each entity has its own Dto equivalent. I can create a user. My issue is trying to log in. My userServiceImpl implements Spring Security UserService. Here is my implementation :
#Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
UserModel userModel = userRepository.findByEmail(email);
if(userModel == null)
throw new UsernameNotFoundException("User with email " + email + " not found");
return new UserPrincipalManager(userModel);
}
My UserPrincipalManager :
public class UserPrincipalManager implements UserDetails {
private static final long serialVersionUID = 7464059818443209139L;
private UserModel userModel;
private ProjectModel projectModel;
#Getter #Setter
private String userId;
#Autowired
public UserPrincipalManager(UserModel userModel) {
this.userModel = userModel;
this.userId = userModel.getUserId();
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new HashSet<>();
Collection<AuthorityModel> authorityModelEntities = new HashSet<>();
// get user roles
Collection<RoleModel> roleModels = userModel.getRoles();
if (roleModels == null) {
return authorities; // null
}
// get user roles
roleModels.forEach((role) ->{
authorities.add(new SimpleGrantedAuthority(role.getName()));
authorityModelEntities.addAll(role.getAuthorities());
});
// get user authorities
authorityModelEntities.forEach(authorityModel -> {
authorities.add(new SimpleGrantedAuthority(authorityModel.getName()));
});
return authorities;
}
#Override
public String getPassword() {
return this.userModel.getEncryptedPassword();
}
#Override
public String getUsername() {
return this.userModel.getEmail();
}
// we do not store this information in DB
#Override
public boolean isAccountNonExpired() {
return true;
}
// we do not store this information in DB (yet)
#Override
public boolean isAccountNonLocked() {
return true;
}
// we do not store this information in DB (yet)
#Override
public boolean isCredentialsNonExpired() {
return true;
}
// isEnabled depending if account is activated => email verification status value
#Override
public boolean isEnabled() {
return this.userModel.getEmailVerificationStatus();
}
}
While trying to log in a User sql request is looping.
at org.modelmapper.internal.converter.MergingCollectionConverter.convert(MergingCollectionConverter.java:59)
at org.modelmapper.internal.converter.MergingCollectionConverter.convert(MergingCollectionConverter.java:31)
at org.modelmapper.internal.MappingEngineImpl.convert(MappingEngineImpl.java:303)
at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:110)
at org.modelmapper.internal.MappingEngineImpl.setDestinationValue(MappingEngineImpl.java:242)
at org.modelmapper.internal.MappingEngineImpl.propertyMap(MappingEngineImpl.java:188)
at org.modelmapper.internal.MappingEngineImpl.typeMap(MappingEngineImpl.java:152)
at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:106)
at org.modelmapper.internal.converter.MergingCollectionConverter.convert(MergingCollectionConverter.java:59)
at org.modelmapper.internal.converter.MergingCollectionConverter.convert(MergingCollectionConverter.java:31)
at org.modelmapper.internal.MappingEngineImpl.convert(MappingEngineImpl.java:303)
at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:110)
at org.modelmapper.internal.MappingEngineImpl.setDestinationValue(MappingEngineImpl.java:242)
at org.modelmapper.internal.MappingEngineImpl.propertyMap(MappingEngineImpl.java:188)
at org.modelmapper.internal.MappingEngineImpl.typeMap(MappingEngineImpl.java:152)
at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:106)
at org.modelmapper.internal.converter.MergingCollectionConverter.convert(MergingCollectionConverter.java:59)
at org.modelmapper.internal.converter.MergingCollectionConverter.convert(MergingCollectionConverter.java:31)
at org.modelmapper.internal.MappingEngineImpl.convert(MappingEngineImpl.java:303)
at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:110)
at org.modelmapper.internal.MappingEngineImpl.setDestinationValue(MappingEngineImpl.java:242)
at org.modelmapper.internal.MappingEngineImpl.propertyMap(MappingEngineImpl.java:188)
at org.modelmapper.internal.MappingEngineImpl.typeMap(MappingEngineImpl.java:152)
at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:106)
In the end the application crashes and returns a 403 error.
2020-10-05 12:07:22.215 DEBUG 4564 --- [nio-8080-exec-8] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-5.3.3.RELEASE.jar:5.3.3.RELEASE]
The login fonction works if user do not have project associated.
I don't know anything about model mapper, but I would like to provide you an alternative solution because I think this is a perfect use case for Blaze-Persistence Entity Views.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(UserModel.class)
public interface UserDto extends Serializable {
#IdMapping
Long getId();
String getUserId();
String getFirstName();
String getLastName();
String getEmail();
String getPassword();
String getEncryptedPassword();
String getEmailVerificationToken();
Boolean getEmailVerificationStatus();
Set<String> getRoles();
Set<ProjectDto> getProjects();
#EntityView(ProjectModel.class)
interface ProjectDto {
#IdMapping
Long getId();
String getProjectId();
// Other mappings...
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
UserDto a = entityViewManager.find(entityManager, UserDto.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
The big bonus here, it will only fetch the columns that are actually needed and it validates the DTO model against your JPA model during boot time, so there are no more runtime surprises!
My User Class looks as follows:
#Entity
#Table(name = "Users")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "userID")
private Integer userID;
#Column(name = "username",nullable = false, unique = true)
private String username;
#Column(name = "password")
private String password;
#Column(name = "name")
private String name;
#Column(name = "address")
private String address;
#Column(name = "email")
private String email;
#OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<CreditCard> creditCard;
//Constructor, Getters and Setters
CreditCard Class looks :
#Entity
#Table(name = "CreditCards")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class CreditCard {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "cardID", nullable = false)
private Integer cardID;
#Column(name = "cardName")
private String cardName;
#Column(name = "cardNumber")
private BigInteger cardNumber;
#Column(name = "expirationDate")
private Integer expirationDate;
#Column(name = "securityCode")
private Integer securityCode;
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "user_id", nullable = false)
#JsonIgnore
private User user;
//Constructor, Getters and Setters
CreditCard Resource:
#RestController
#RequestMapping("/geektext/users")
class CreditCardResource {
#Autowired
CreditCardRepository cardsRepository;
#Autowired
UserRepository userRepository;
//Displays CreditCard By Username Search
#GetMapping("/{username}/cards")
public Optional<CreditCard> getCardsByUsername(#PathVariable String username) throws NotFoundException {
if (!userRepository.findByUsername(username).isPresent()){
throw new NotFoundException("User '" + username + "' not found");
}
return cardsRepository.findById(userRepository.findByUsername(username).get().getUserID());
}
//Creates New Card for User
#PostMapping("/{userID}/cards")
public CreditCard loadCard(#PathVariable String userID, #RequestBody CreditCard creditCard) throws NotFoundException {
return userRepository.findByUsername(userID).map(user -> {creditCard.setUser(user);
return cardsRepository.save(creditCard);
}).orElseThrow(() -> new NotFoundException("User '" + userID + "' not found"));
}
}
There is also a UserResource.java , UserRepository (Interface) and CreditCardRepository) but these do not affect the problem I am having. Please how can I fix getting list of cards for User passing username on url. How can user create New/ More than one CreditCard instead of updating the one he has.
You are trying to get a credit-card using your userID
return cardsRepository.findById(userRepository.findByUsername(username).get().getUserID());
Instead, you could search for your credit-card by user. To do this, you should create a method in the credit-card repository interface.
List<CreditCard> findByUser(User user);
Then call this method from your controller
return cardsRepository.findByUser(userRepository.findByUsername(username).get())
The post method has a similar problem. You are trying to get user by username, but passing the userID. Also you set user to your new credit-card, but you don't add a new credit-card to your user. (And change the name of credit-cards variable in the User class to creditCards)
return userRepository.findByUsername(userID).map(user -> {creditCard.setUser(user);
return cardsRepository.save(creditCard);
}).orElseThrow(() -> new NotFoundException("User '" + userID + "' not found"));
This will be much better. Test it yourself and change something if I wrote something wrong
User user = userRepository.findById(userID);
user.getCreditCards().add(creditCard);
creditCard.setUser(user);
userRepository.save(user);
NotFoundException I guess you can handle by yourself.
Update: I had to create an ID for each credit card since if the same ID is assigned on the creation of each new credit card, then program would treat it like if I was the same one that was trying to be updated.
In my current implementation I have a User entity that implements org.springframework.security.core.userdetails.UserDetails interface.
#Entity
#Table(name = "users")
public class User extends BaseEntity implements UserDetails {
private static final long serialVersionUID = 8884184875433252086L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String username;
private String password;
....
During the OAuth2 authorization I manually create a new User object, populate its fields and store in my database.
According to the UserDetails contract - UserDetails.getUsername() method can't return null but I have no values retrieved from Social Networks that can be used as username.
What value in this case should be returned in the User.getUsername() method ?
Is it okay to return something like this ?
#Override
public String getUsername() {
return String.valueOf(id);
}
If you need to save this entity before you have a valid value for the name then I think that's a problem with the design. Having said that User.getUsername() is mainly used for display purposes, so I doubt it matters what the actual value is, as long as it can be matched to something in an authentication.