Spring cache try and cache user - spring

I am trying to cache the UserDetails loadUserByUsername(String username)
the problem is that after the caching the results comes with the correct user but
the password is always set to null but it was not null when cached
#Service
public class MyUserDetailsService implements UserDetailsService {
#Autowired
UserRepository userRepository;
#Cacheable(value="usersLogged" ,key="#username" ,unless="#result.password==null")
#Override
public org.springframework.security.core.userdetails.User loadUserByUsername(
String username) throws UsernameNotFoundException {
try {
// User user = userRepository.getUserByEmail(username); Switch to id
// token base
User user = userRepository.findOne(username);
if (user == null) {
throw new UsernameNotFoundException(
"Invalid username/password.");
}
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = user.isActive();
String userN = user.getId(); // the suer is in the system
String pass = user.getPassword();
Collection<? extends GrantedAuthority> authorities = AuthorityUtils
.createAuthorityList(user.getRole().toString());
org.springframework.security.core.userdetails.User userBuild = new org.springframework.security.core.userdetails.User(
userN, pass, user.isEnabled(), accountNonExpired,
credentialsNonExpired, accountNonLocked, authorities);
return userBuild;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
// throw new
// UsernameNotFoundException("Invalid username/password.");
}
}
}

Seems like spring cache have problems caching when public visibility
password is protected
In the manual
When using proxies, you should apply the cache annotations only to methods with public visibility. If you do annotate protected, private or package-visible methods with these annotations, no error is raised, but the annotated method does not exhibit the configured caching settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods as it changes the bytecode itself

Related

How to provide custom UserDetails with additional fields for testing a secured controller method?

Assume I have the following #WebMvcTest and #RestController in a Spring boot applcation (version 2.4.2).
// the test
#Test
#WithUserDetails
public void should_return_ok() throws Exception {
mockMvc.perform(get("/api/products").andExpect(status().isOk());
}
// the controller
#GetMapping(path = "/api/products")
public ResponseEntity<List<Product>> getProducts(#AuthenticationPrincipal CustomUserDetails userDetails) {
List<Product> products = productService.getProductsByUserId(userDetails.getUserId());
return ResponseEntity.ok(products);
}
I also provided a CustomUserDetails class which adds a userId.
#Getter
#Setter
public class CustomUserDetails extends User {
private static final long serialVersionUID = 5540615754152379571L;
private Long userId;
public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public CustomUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
}
I understand that Spring provides the #WithUserDetails annotation to provide an adequate object for testing. And this also allows specifying a custom username, password, etc. However I don't know how I could provide the userId which is necessary so that the controller method can extract it from the CustomUserDetails object.
You can create your own custom UserDetails object in your test class and do the following:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
CustomUserDetails customUserDetails = new CustomUserDetails(...);
mockMvc.perform(get("/api/products").with(user(customUserDetails))).andExpect(status().isOk());
In your implementation of UserDetailsService you should return your instance of UserDetails. For example:
#Override
public UserDetails loadByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("Username " + username + " not found");
}
CustomUserDetails customUserDetails = new CustomUserDetails(user);
customUserDetails.setUserId(user.getUserId());
return customUserDetails;
}
public class CustomUserDetails implements UserDetails {
private final Long userId;
private final User user;
...constructors
...getters and setters
}
In your code, you can cast the Authentication object to your CustomUserDetails.
CustomUserDetails customUserDetails = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication();
Long userId = customUserDetails.getUserId();

How to create multiple implementations of UserDetailsService in Spring Boot

I want to customize login API in spring boot. For a single kind of user, I created a implementation of UserDetailsService and it worked perfectly fine. Now, I want to create 3 different kinds of users, i.e., 3 different authorities. I don't think a single implementation can help me here. If I create 3 different implementations, and try using #Qualifier, how do I call a specific implementation ?
Any sort of help is appreciated! Below is the code for Login Endpoint of single kind Of user.
private static Logger logger = LogManager.getLogger();
#Value("${jwt.expires_in}")
private int EXPIRES_IN;
#Autowired
AuthenticationManager authManager;
#Autowired
TokenHelper tokenHelper;
#Autowired
ObjectMapper objectMapper;
#Autowired
PrincipalRepository principalRepository;
private boolean isAuthenticated(Authentication authentication) {
return authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated();
}
#PostMapping("/principal")
public ResponseEntity<Object[]> loginPrincipal(#RequestParam(name ="username") String username,
#RequestParam(name ="password") String password){
logger.info("In login api");
if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
logger.error("Invalid Request!");
return ResponseEntity.badRequest().header("reason", "bad request").body(null);
}
UsernamePasswordAuthenticationToken authReq =
new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authManager.authenticate(authReq);
boolean isAuthenticated = isAuthenticated(authentication);
if (!isAuthenticated) {
logger.error("Not authenticated");
return ResponseEntity.badRequest().body(null);
}
Principal principal = null;
try {
principal = principalRepository.findByUserName(username);
}catch(Exception e) {
logger.error("Couldn't retrieve user");
return ResponseEntity.badRequest().header("reason", "username not found").body(null);
}
String jwt = tokenHelper.generateToken( username );
SecurityContextHolder.getContext().setAuthentication(authentication);
UserTokenState userTokenState = new UserTokenState(jwt, EXPIRES_IN);
return ResponseEntity.accepted().body(new Object[] {userTokenState, principal.getPrincipalID()});
}
Below is the code for UserDetailsService Implementation:
#Service
public class UserDetailServiceImpl implements UserDetailsService {
#Autowired
private PrincipalRepository principalRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("Loading user from db");
Principal principal = principalRepository.findByUserName(username);
if( principal == null){
System.out.println("User not found");
throw new UsernameNotFoundException("No user found. Username tried: " + username);
}
Set<GrantedAuthority> grantedAuthorities = new HashSet<GrantedAuthority>();
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_PRINCIPAL"));
System.out.println("All done");
return new org.springframework.security.core.userdetails.User(principal.getUserName(), principal.getPassword(), grantedAuthorities);
}
}
Here, I am fetching a principal from db, because this implementation is principal-specific. I wanna create similar implementations for Student and Teacher and use them accordingly.
You don't need to create more than one implementation for UserDetailsService. Student, Teacher are also users, only one thing will differ these users is "authorities"(role & authorities) in the application if we look at from general view. Spring Security firstly checks "username" and "password" for authentication and after successful authentication, it checks "authorities" for authorization process in order to allow to use resources(methods, and etc) according to the business logic of the application.

How to get client IP address in ImpUserDetailsService

I want get client IP address in method loadUserByUsername() from class implUserDetailsService this my code but it doesn't work
#Service
public class LoginServiceImpl implements UserDetailsService {
#Autowired
UserDao loginDao;
#Autowired
private HttpServletRequest request;
#Override
public UserDetails loadUserByUsername(String username) {
try {
final String ip = getClientIp(request);
net.liyan.psc.main.entity.main.User user = loginDao.findByUserNameForLogin(username);
if (user == null) throw new UsernameNotFoundException("User not found.");
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
if (isLocalZone()) {
grantedAuthorities.add(new SimpleGrantedAuthority('ROLE_1'));
} else {
grantedAuthorities.add(new SimpleGrantedAuthority('ROLE_2'));
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
true,
true,
true,
true,
grantedAuthorities);
} catch (UsernameNotFoundException ex) {
throw new UsernameNotFoundException("User not found.");
} catch (Exception ex) {
throw new UsernameNotFoundException("User not found.");
}
}
private static String getClientIp(HttpServletRequest request) {
String remoteAddr = "";
if (request != null) {
remoteAddr = request.getHeader("X-FORWARDED-FOR");
if (remoteAddr == null || "".equals(remoteAddr)) {
remoteAddr = request.getRemoteAddr();
}
}
return remoteAddr;
}
private boolean isLocalZone(String Ip){
// ...
}
}
It get exseption:
java.lang.IllegalStateException: No thread-bound request found: Are
you referring to request attributes outside of an actual web request,
or processing a request outside of the originally receiving thread? If
you are actually operating within a web request and still receive this
message, your code is probably running outside of
DispatcherServlet/DispatcherPortlet: In this case, use
RequestContextListener or RequestContextFilter to expose the current
request.
There are various options to make #Autowired able to inject HttpServletRequest into a bean:
Register RequestContextListener
Register RequestContextFilter. Make sure it is placed at the very beginning of the filter chain (e.g. Before springSecurityFilterChain)
If you are using Spring boot and have a spring-mvc on the classpath , its auto-configuration should register one RequestContextFilter for you by default.
Change the loadUserByUsername(String username) to
loadUserByUsername(String username, HttpServletRequest request)
Pass the request from controller end to your service end. Something like below,
import javax.servlet.http.HttpServletRequest;
#Controller
public class YourControllerName {
#Autowired
UserDetailsService userDetailsService
#GetMapping("/your-url")
public String methodName(HttpServletRequest request /*your other perams*/){
UserDetails userDetails = userDetailsService .loadUserByUsername(String
username, request);
//other operations
return "view";
}
}
Remove the HttpServletRequest autowire from service end.

How to add additional details to Spring Security userdetails

I want to add additional information to userdetails like user's Ip address. Is there any way to achieve this? I tried to create a new CustomSpringUser class but the problem is how can i get this information from Authentication object. Is there any other way to store additional information for authenticated user?
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
My custom user class;
public class CustomSpringUser extends org.springframework.security.core.userdetails.User {
public String ip;
public CustomSpringUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public CustomSpringUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities, String ip) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.ip= ip;
}
}
Edit: I found that we can add additional information for Authentication but I couldn't found how to do that.
http://docs.spring.io/spring-security/site/docs/3.0.x/apidocs/org/springframework/security/core/Authentication.html#getDetails()
#Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
try {
AppUser appUser = new AppUser();
appUser.setUsername(userName);
AppUser domainUser = genericDao.getByTemplate(appUser).get(0);
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
List<String> roles = new ArrayList<String>();
roles.add(domainUser.getRole().getName());
return new CustomSpringUser(
domainUser.getUsername(),
domainUser.getPassword().toLowerCase(),
enabled,
accountNonExpired,
credentialsNonExpired,
accountNonLocked,
getGrantedAuthorities(roles),
***domainUser.getAccount().getIdentificationId())*** ;
} catch (Exception e) {
genericLogger.saveLog(Logger.Status.ERROR, "Couldn't login", e);
throw new RuntimeException(e);
}
}
To get the UserDetails from the Authentication object/instance use the getPrincipal() method. The getDetails() method is to be used to get additional information about the user (which in general will be an instance of WebAuthenticationDetails).
Links
Authentication javadoc
Authentication.getDetails() javadoc
Authentication.getPrincipal() javadoc

UserDetailsService config for properly getting user

I create this topic from my previous one Get authenticated user entity Spring MVC where I asked question about properly getting authenticated user entity. I adviced that Principal object (for example, on my view <sec:authentication property="principal.customFieldName" />) can has access to my custom fields if my UserDetailsService configuration is right. Does my UserDetailsService configured properly to accomplish this functionality?
#Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
private static final Logger logger = Logger.getLogger(UserDetailsServiceImpl.class);
#Autowired
#Qualifier("hibernateUserDao")
private UserDAO userDAO;
#Override
#Transactional(readOnly = true)
public org.springframework.security.core.userdetails.UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException, DataAccessException {
UserDetails user = userDAO.findByLogin(userName);
if (user == null) {
logger.error("User was not found! Input login: " + userName);
}
return buildUserFormUserEntity(user);
}
#Transactional(readOnly = true)
private org.springframework.security.core.userdetails.User buildUserFormUserEntity(UserDetails userDetails) {
boolean enableStatus = userDetails.isEnabled();
String userName = userDetails.getLogin();
String password = userDetails.getPassword();
boolean enabled = enableStatus;
boolean accountNonExpired = enableStatus;
boolean credentialsNonExpired = enableStatus;
boolean accountNonLocked = enableStatus;
Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(userDetails.getRole()));
User springSecurityUser = new User(userName, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
return springSecurityUser;
}
public UserDAO getUserDAO() {
return userDAO;
}
public void setUserDAO(UserDAO userDAO) {
this.userDAO = userDAO;
}
}
I think you need some additional steps to be able succesfully use
<sec:authentication property="principal.customFieldName" />
on some page:
Add your custom user object that implements org.springframework.security.core.userdetails.UserDetails interface. The simpliest way to do it is to extend existing org.springframework.security.core.userdetails.User class: class CutomUser extends User
Add your customFieldName property to CutomUser class.
Use CutomUser as a return type in your UserDetailsServiceImpl.loadUserByUsername(...) method. Do not forget to fill customFieldName at this moment.

Resources