Spring boot UserDetailsService Multi-User with extra fields - spring-boot

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;
}
}

Related

Confused why getting a User from Repository fixed "failed to lazily initialize a collection of role" compared to using SecurityContextHolder

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
}

JPA OneToOne and shared primary key need manual assignment

I'm using Springboot and JPA to create two tables sharing the same primary key.
For the first table I write:
public class UserAccount implements Serializable
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToOne(mappedBy ="user", cascade = {CascadeType.REMOVE, CascadeType.MERGE,
CascadeType.REFRESH}, fetch=FetchType.LAZY)
#PrimaryKeyJoinColumn
private UserLogin login;
}
For the second table I write:
public class UserLogin implements Serializable
{
#Id
private Long user_id;
#OneToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH},
fetch=FetchType.LAZY)
#MapsId("user_id")
#JoinColumn(name = "user_id", referencedColumnName = "id")
#Setter(AccessLevel.NONE)
private UserAccount user;
public void setUser(UserAccount user)
{
this.user = user;
this.user_id = user.getId();
}
}
Other stuff are omitted for conciseness. The code works because I manually set the id of UserLogin by writing the statement
this.user_id = user.getId();
otherwise I get the error:
Hibernate error: ids for this class must be manually assigned before calling save():
I guess that the ids can be manually managed but I cannot get the right configuration.
UPDATE:
I found the solution thanks (see the accepted answer). Now I just would get rid of the findById() when setting the user login.
//these methods are defined within a dedicated #Service
#Transactional
public void createLoginInfo(UserAccount user)
{
UserLogin userlogin=new UserLogin();
this.addLoginToUser(userlogin,user);
loginService.save(userlogin);
}
#Transactional
public void addLoginToUser(UserLogin login, UserAccount account)
{
//whit this commented line works
//UserAccount acc= this.findById(account.getId());
login.setUser(account);
account.setLogin(login);
}
//In a transactional test method I first create the user then I call
userService.save(theuser);
userService.createLoginInfo(theuser);
You have a bidirectional relationship, but have mapped it with a few competing options that don't work well together. First, in UserAccount, it isn't clear why you have an ID that is generated, yet try to also map it with the relationship (specifically using a PrimaryKeyJoinColumn). If you want it generated, it can't also be a foreign key value in a reference - and you've already got this relationship setup as the 'other' side via the 'mappedBy' setting. It should just be:
public class UserAccount implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToOne(mappedBy ="user", cascade = {CascadeType.REMOVE, CascadeType.MERGE,
CascadeType.REFRESH}, fetch=FetchType.LAZY)
private UserLogin login;
}
User login then should just be:
public class UserLogin implements Serializable {
#Id
private Long user_id;
#OneToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH},
fetch=FetchType.LAZY)
#MapsId("user_id")
#JoinColumn(name = "user_id", referencedColumnName = "id")
#Setter(AccessLevel.NONE)
private UserAccount user;
public void setUser(UserAccount user) {
this.user = user;
}
}
Note because you have the mapsId annotation on the user relationship, JPA will set the user_id property for you once the ID is assigned - there is no need to manually set it yourself. You can, but if you do you must insure it was assigned previously - which requires a save/flush on the UserAccount. If you don't actually use the Long user_id property, you don't really even need to map it; you can just mark the user property as the ID:
public class UserLogin implements Serializable {
#Id
#OneToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH},
fetch=FetchType.LAZY)
#JoinColumn(name = "user_id", referencedColumnName = "id")
#Setter(AccessLevel.NONE)
private UserAccount user;
public void setUser(UserAccount user) {
this.user = user;
}
}
The Long ID from UserAccount then can be used to lookup UesrAccounts and UserLogin instances.
Try this :
public class UserLogin implements Serializable
{
#Id
private Long user_id;
#OneToOne(fetch=FetchType.LAZY)
#MapsId
#JoinColumn(name = "user_id")
private UserAccount user;
public UserAccount getUser() {
return user;
}
public void setUser(UserAccount user) {
this.user = user;
}
}
public class UserAccount implements Serializable
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
To persist UserLogin :
EntityManager em;
UserAccount user = em.find(UserAccount.class, 1L)
UserLogin login = new UserLogin();
login.setUser(user);
em.persist(login);

#GetMapping doesn't display CreditCards under username. #PostMapping doesn't create a new card for user, it only updates it

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.

ClassCastException - Spring Security With User

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);

Spring boot JPA many to many with extra column insert and update issue

Here is my initial question.
Spring Data JPA Many to Many with extra column User and Roles
Now I have the right tables created, but can't make it work for the update.
Here is the code:
User.java
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
#OneToMany(mappedBy="user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<UserRole> roles;
// getters and setters
}
Role.java
#Entity
#Table(name = "roles")
public class Role {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// getters and setters
}
UserRole.java
#Entity
#Table(name = "users_to_role")
public class UserRole implements Serializable {
#Id
#ManyToOne
#JoinColumn(name = "user_id")
private User user;
#Id
#ManyToOne
#JoinColumn(name = "role_id")
private Role role;
private Date createdAt;
public UserRole(){}
public UserRole(User user, Role role, Date date) {
this.user = user;
this.role = role;
this.createdAt = date;
}
// getters and setters
}
Controller
#RestController
public class APIController {
#Autowired
RoleRepository roleRepository;
#Autowired
UserRepository userRepository;
#ResponseBody
#RequestMapping(value = "create", method = RequestMethod.GET)
public String create(){
//Insert test - WORKING BUT NOT SURE IF ITS RIGHT WAY
List<UserRole> userRoles = new ArrayList<>();
Role role = roleRepository.getOne((long) 1);
//Create user
User user = new User();
user.setUsername("test");
//Create userRole
userRoles.add(new UserRole(user, role, new Date()));
user.setRoles(userRoles);
userRepository.save(user);
return "created";
}
#ResponseBody
#RequestMapping(value = "edit", method = RequestMethod.GET)
public String edit(){
//Edit test - NOT working
List<UserRole> userRoles = new ArrayList<>();
Role role = roleRepository.getOne((long) 2);
//get user from db
User user = userRepository.getOne((long) 1);
//Create userRole
userRoles.add(new UserRole(user, role, new Date()));
// WAS FIRST ATTEMPT using user.setRoles(userRoles); but got error and use
//https://stackoverflow.com/questions/9430640/a-collection-with-cascade-all-delete-orphan-was-no-longer-referenced-by-the-ow
//user.setRoles(userRoles);
user.getRoles().clear();
user.getRoles().addAll(userRoles);
userRepository.save(user);
return "done";
}
}
I am getting this error:
com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Column 'user_id' cannot be null

Resources