A guy ask me for help him resolve a strange bug in security service of his spring boot app, after hours of trail I manage to fix the bug but I really have no idea what happen. Please look at these classes:
Class User : user infomation in database
#Getter
#Setter
#Entity
#Table(name = "user", uniqueConstraints = {#UniqueConstraint(columnNames = {"user_name"})})
public class User extends DateAudit implements Serializable {
// Id, username, password and constructor... not really important
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "user_roles", joinColumns = #JoinColumn(name = "user_id"), inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> role = new HashSet<>();
}
Class UserDto : implement Spring's interface for manage user credential
#Getter
#Setter
#AllArgsConstructor
public class UserDto implements org.springframework.security.core.userdetails.UserDetails {
private long id;
private String Username;
#JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public static UserDto create(User user) {
List<GrantedAuthority> authorities = user.getRole().stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name())).collect(Collectors.toList());
return new UserDto(user.getId(), user.getUserName(), user.getPassword(), authorities);
}
// implement interface's methods, only getters, not important
}
Class CustomUserService : Auth service
#Service
public class CustomUserService implements org.springframework.security.core.userdetails.UserDetailsService {
private final UserRepository userRepository;
#Autowired
public CustomUserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
#Override
#Transactional
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
User user = userRepository.findUserByUserName(userName).orElseThrow(() -> {
return null;
});
return UserDto.create(user);
}
}
Full source code can be found here (it is only a simple spring boot app with some classes) : https://github.com/raizo000/admin_project (his repo uses embedded tomcat, I've tried to change it to jetty but it is not the cause)
When I run code the first time with these 3 classes. The line
return UserDto.create(user);
Give me a NoClassDefFoundError :
WARN 7252 --- [qtp223566397-22] org.eclipse.jetty.server.HttpChannel : /login
java.lang.NoClassDefFoundError: com/example/admin/dto/UserDto
at com.example.admin.services.CustomUserService.loadUserByUsername(CustomUserService.java:28) ~[classes/:na]
at com.example.admin.services.CustomUserService$$FastClassBySpringCGLIB$$b9234a59.invoke(<generated>) ~[classes/:na]
....
I have checked the jar file, there is a UserDto.class file in the right directory.
Remove the #Transactional help fix the error but cause another lazily initialize error and I end up change fetch = FetchType.LAZY in User class to fetch = FetchType.EAGER as a quick fix.
Why adding Transactional can cause a NoClassDefFoundError? Do remove Transactional is the right solution or there is a better fix?
When a class is not found, the first thing that you should make is clean and build.
Because, the last build is saving some dependency.
So the #Transactional #interface was not found in some corrupt package.
A rebuild(clean build) solves it.
Related
I want to test Transactional operation in my project. Basically, I want to roll back the userService.saveUser() operation, if an exception is thrown. I have simplified the classes, and you can find it below.
A user must live in an address. An address can have multiple users.
Address Entity
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
#Entity
public class Address {
#Id
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "STREET")
#NotNull
private String street;
#ToString.Exclude
#OneToMany(mappedBy = "address")
private Set<User> users = new HashSet<>();
}
User Entity
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
#Entity
#Table(name = "USER")
public class User {
#Id
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "FIRSTNAME", nullable = false)
#NotNull
private String firstName;
#ManyToOne(fetch = FetchType.LAZY)
private Address address;
}
Repositories
public interface AddressRepository extends CrudRepository<Address, Long> {
}
public interface UserRepository extends CrudRepository<User, Long> {
}
UserService Class
#Service
#Slf4j
#AllArgsConstructor
public class UserService {
#Autowired
AddressRepository addressRepository;
#Autowired
UserRepository userRepository;
#Transactional
public void saveUser(String firstName, String street) {
var address1 = Address.builder.street(street).build();
// to make sure that I have "id" of the address when I am saving it.
var addressSaved = addressRepository.save(address1);
if ("f1".equals(firstName))
throw new RuntimeException("some exception");
var user = User.builder()
.firstName(firstName)
.address(addressSaved)
.build();
// this operation can also throw DataIntegrityViolationException
userRepository.save(user);
}
}
This is my test class
#SpringBootTest
class UserServiceIT {
#Autowired
AddressRepository addressRepository;
#Autowired
UserRepository userRepository;
#Autowired
UserService userService;
#BeforeEach
void beforeEach() {
userRepository.deleteAll();
addressRepository.deleteAll();
}
#Test
void test_saveUser() {
assertThrows(RuntimeException.class,() -> userService.saveUser("f1", "s1"));
assertEquals(0, userRepository.count());
assertEquals(0, addressRepository.count());
}
#Test
void test_saveUser2() {
// column: nullable = false will throw the exception
assertThrows(DataIntegrityViolationException.class,() -> userService.saveUser(null, "s1"));
assertEquals(0, userRepository.count());
assertEquals(0, addressRepository.count());
}
}
Both of the tests give assertion error on address count (Address is saved and user is not saved). I expect address to be roll backed (and not to be saved) since there is an error after saving the address, and while saving the user (some condition is violated, therefore 2 saves must be roll backed). What am I doing wrong?
application.yml for test environment
spring:
devtools:
restart:
enabled: false
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=false
driverClassName: org.h2.Driver
username: sa
password: 123
h2:
console:
enabled: false
jpa:
database-platform: org.hibernate.dialect.H2Dialect
database: H2
show-sql: false
hibernate:
ddl-auto: create
You can reach the whole sample project from this link: https://wetransfer.com/downloads/7cb870266e2e20f610b44d3cc9f229c220220308071438/7b88a2700076a3e53771e389c796cfe420220308071438/c777ab
The code you posted here differs from what is actually exists in the original code you uploaded.
original code:
#Transactional
void saveUser(String firstName, String street) {
var address = Address.builder().street(street).build();
var addressSaved = addressRepository.save(address);
if ("f1".equals(firstName))
throw new RuntimeException("f1");
var user = Person.builder()
.firstName(firstName)
.address(addressSaved)
.build();
personRepository.save(user);
}
This method actually have default access modifier so CGLIB is not able to override it and creates the needed logic. change access modifier of this method to public
I want to make login form with data from my own database. I made Entity User class:
#Entity
#Table(name = "Auth_data")
public class User {
#Id
#GeneratedValue
#Getter #Setter
private Long id;
#Column(name= "username")
#Getter #Setter
private String username;
#Column(name="password")
#Getter #Setter
private String password;
#Enumerated(EnumType.ORDINAL)
#Column(name="role")
#Getter #Setter
private Role role;
#Column(name = "client_ID")
#Getter #Setter
private Long clientID;
#Column(name = "instructor_ID")
#Getter #Setter
private Long instructorID;
public User() {}
public User(String username, String password, Role role, Long clientID, Long instructorID) {
this.username = username;
this.password = password;
this.role = role;
this.clientID = clientID;
this.instructorID = instructorID;
}
}
Also I made UserDetailsServiceIml class which implements UserDetailsService:
#Service
public class UserDetailsServiceIml implements UserDetailsService {
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
final User user = userRepository.findByUsername(username);
if(user == null) {
throw new UsernameNotFoundException(username);
}
Set<GrantedAuthority> grantedAuthorities = new HashSet< >();
grantedAuthorities.add(new SimpleGrantedAuthority(user.getRole().name()));
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),
grantedAuthorities);
}
}
My User repository class:
#Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
I also tried make Query like this:
#Repository
public interface UserRepository extends JpaRepository<User, Long> {
#Query("Select u from User u WHERE u.username=:username")
User findByUsername(#Param("username") String username);
}
But when I enter login and password into login form I get error, because User is Null after searching in database. How can I fix it?
UPD: SOLVED!
I don't know why and how. I found that hibernate created new table "auth_data", but not used my table "Auth_data". So, I renamed "Auth_data" to "auth_data" and now it's work! I also changed #Getter #Setter on #Data,changed in User repository on " User findByUsername(String username);" and changed type of field role from enum to String.
It is possible that you don't seed the database with the user you are trying to login. You can use an Application Runner class to seed it on every start if the user doesn't exist in the database. Also, you can apply the #Data annotation from Lombok on class level to generate getters and setters for all the fields.
package com.javahowtos.dataseeddemo.dataseed;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import com.javahowtos.dataseeddemo.model.User;
import com.javahowtos.dataseeddemo.repository.UserRepository;
#Component
public class UserDataLoader implements CommandLineRunner {
#Autowired
UserRepository userRepository;
#Override
public void run(String... args) throws Exception {
loadUserData();
}
private void loadUserData() {
if (userRepository.count() == 0) {
User user1 = new User("John", "Doe");
User user2 = new User("John", "Cook");
userRepository.save(user1);
userRepository.save(user2);
}
System.out.println(userRepository.count());
}
}
Source: https://javahowtos.com/guides/107-spring/376-how-to-seed-the-database-in-spring-boot-project.html
You have to be sure that you have a user with the appropriate username in the database (case sensitive), if you do not have a user you can insert some test users (with the migration tool, with custom component, or manually)
You probably need to add a unique index for username
If you extend JpaRepository and add method findByUsername spring will generate implementation and you do not need #Query, if you have multiple items you can use findFirstByUsername
This question already has answers here:
How to fix Hibernate LazyInitializationException: failed to lazily initialize a collection of roles, could not initialize proxy - no Session
(15 answers)
Closed 2 years ago.
I am trying to fetch user's profile data post login (from login success filter) but I am seeing an exception for Lazy loading the data. Please see the following sample code:
AuthenticationSuccessHandler.java
#Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
#Autowired
private UserService userService;
#Autowired
private Gson gson;
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
User user = (User) authentication.getPrincipal();
UserLoginResponseDto userLoginResponseDto = userService.login(user.getUsername());
response.setStatus(HttpStatus.OK.value());
response.setContentType("application/json; charset=UTF-8");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.getWriter().println(gson.toJson(userLoginResponseDto));
response.getWriter().flush();
clearAuthenticationAttributes(request);
}
}
UserService.java
public class UserService implements UserDetailsService, TokenService {
#Autowired
private UserRepository userRepository;
#Transactional(readOnly = true)
public UserLoginResponseDto login(String email) {
Optional<UserModel> userOptional = userRepository.findByEmailIgnoreCase(email);
UserModel userModel = userOptional.get();
UserLoginResponseDto userLoginResponseDto = userModel.toUserLoginResponseDto();
return userLoginResponseDto;
}
}
UserModel.java
public class UserModel {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(nullable = false, unique = true, updatable = false)
private UUID id;
[A FEW MORE FIELDS]
#Column(length = 256, nullable = false, unique = true, updatable = false)
private String email;
#OneToMany(cascade = { CascadeType.ALL })
private List<RoleModel> roleModels;
public UserLoginResponseDto toUserLoginResponseDto() {
return new UserLoginResponseDto().setId(id).setEmail(email).setRoles(roleModels);
}
}
UserLoginResponseDto.java
public class UserLoginResponseDto {
private UUID id;
private String email;
private List<RoleModel> roles;
}
When an object of type UserLoginResponseDto is serialized in AuthenticationSuccessHandler, I see the following error message -
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: UserModel.roleModels, could not initialize proxy - no Session
QQ - How do I resolve this correctly without employing any of the following techniques?
[ANTIPATTERN] Open-In-View
[ANTIPATTERN] hibernate.enable_lazy_load_no_trans
FetchType.EAGER
Your problem is that you're passing the actual lazy List into setRoles, which doesn't trigger the full load. This indicates (immediately) that while you've separated your top-level database class from your top-level DTO, it's a "shallow" separation, which doesn't fully materialize the values. You haven't shown whether RoleModel is an entity or an embeddable, and that matters.
So the first step is to copy the items into a non-JPA form. If RoleModel embeddable (essentially a POJO), this could be as simple as setRoles(new ArrayList<>(roles)). Otherwise, you need a nested DTO, and at that point might consider something like MapStruct.
In either case, though, you're likely to run into the N+1 problem. You in fact do want an eager fetch in this case, and that's what JPA entity graph is for. You can tell Spring Data to fetch the list eagerly only when you want it, and this is a perfect example of when to do that.
Let's say I have 2 have to entities:
#Entity
public class Post {
#NotEmpty
private String title;
#NotEmpty
#Lob
private String html;
#NotEmpty
#Lob
private String text;
#ManyToOne
private Topic topic;
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name = "content_media", joinColumns = {#JoinColumn(name = "content_id")}, inverseJoinColumns = {#JoinColumn(name = "media_id")})
private Set<Media> medias = new HashSet<>();
#CreatedBy
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn
private User createdBy;
#LastModifiedBy
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn
private User lastModifiedBy;
...
}
#Entity
public class Media {
#NotEmpty
private String localPath;
#NotEmpty
private String fileName;
private long fileLength;
private String fileType;
private int focusPointX;
private int focusPointY;
...
}
And I'm exposing them using:
#RepositoryRestController
public interface MediaRepository extends JpaRepository<Media, Long> {
}
#RepositoryRestController
public interface PostRepository extends JpaRepository<Post, Long> {
}
I want these controllers to be secure. Let me explain myself.
If logged in user does not have ROLE_ADMIN, Medias should only be
accessable through posts and /medias/ should return 403 or 404
Only users that have ROLE_USER should be able to create to posts
Only the user that have created the post or the ones that have the ROLE_ADMIN should be able to update post.
Only the users that have ROLE_ADMIN should be able to delete posts
Is there a way to do these using RepositoryRestController and Spring Security or RepositoryRestController is for public resources only and I should write service layer myself using RestController?
Yes you can directly use Spring Security with Spring Data REST. You need to define the security of your routes using Spring Security Configuration as shown below:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests().
antMatchers(HttpMethod.POST, "/posts").hasRole("USER").
antMatchers(HttpMethod.DELETE, "/posts/**").hasRole("ADMIN").and().
csrf().disable();
}
}
Repository methods will be secured using Spring Security annotations. e.g.
#RepositoryRestController
public interface PostRepository extends JpaRepository<Post, Long> {
#Override
#PreAuthorize("hasRole('ROLE_ADMIN')")
void delete(Long aLong);
}
Code above is just a pointer. You can customize it as per your needs. Here is the link to Spring Data examples repository.
Update
To handle the update of the post by the user who created or by any user who is in ADMIN_ROLE you need to create a controller class and define a method with to handle the update
#RequestMapping(method={RequestMethod.PUT}, value={"posts/{id}"})
public void updatePost(#PathVariable("id") Long id, HttpServletRequest request)
{
//Fetch the authenticated user name
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
}
// Make a database call to verify if the user is owner of the post
Post post = postRepository.getPostByUserName(String username, Long postId);
if (post == null && !request.isUserInRole("ADMIN");) {
//return 403 error code
}
//proceed with the update
}
My program has 2 entities:
#Entity
public class User implements Serializable {
#ManyToMany(cascade={CascadeType.ALL},fetch = FetchType.EAGER,
targetEntity = Ip.class,mappedBy = "user")
#Cascade(org.hibernate.annotations.CascadeType.ALL)
#Fetch(value = FetchMode.SUBSELECT)
private Collection<Ip> allowedIp;
...
}
#Entity
public class Ip implements Serializable {
#Column(unique=false,updatable=true,insertable=true,nullable=true,
length=255,scale=0,precision=0)
#Id
private String value;
#ManyToMany(targetEntity = User.class)
private Collection<User> user;
...
}
I'm trying to persist a new User using a Spring JPA repository like the following:
#Transactional (readOnly = false)
#Repository
public interface UserRepository extends JpaRepository<User, String> {
}
List<Ip> allowedIp = new ArrayList<Ip>();
allowedIp.add(ipRepository.findOne("*"));
User user = new User();
user.setAllowedIp(allowedIp);
userRepository.save(user);
The problem is that Ip (the * value) is never persisted although if I added the JPA cascading annotation, and also the hibernate cascading annotation. Any idea why this problem is happening ?
You marked the #manytomany as mannaged by the IP class (mappedBy annotation). You have to add the User to the IP.user collection to have the reletionship persisted.
for (Ip ip : allowedIp) ip.getUser().add(user)