I have a web application with spring, one rest endpoint receives a value encrypted like this:
#RequestMapping(value = "/welcome", method = RequestMethod.GET)
public resp welcome(
#RequestHeader(name = "user") String user,
#Encripted #RequestHeader(name = "password") String password
I'm creating the annotation #Encripted like this:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.PARAMETER)
public #interface Encripted {
}
now I need to manipulate the String that got annotated and change its value, how could I override or implement to achieve this goal? I can't find example with ElementType.PARAMETER,
Lets say you created WelcomeController and Encrypted classes in com.example.controller like so,
package com.example.controller;
public class WelcomeController {
#RequestMapping(value = "/welcome", method = RequestMethod.GET)
public void welcome(
#RequestHeader(name = "user") String user,
#Encrypted #RequestHeader(name = "password") String password
}
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.PARAMETER)
public #interface Encrypted {
String value() default "";
}
In order to process the Annotation you need to create an Aspect class like so --
#Aspect
public class MyAspect {
#Pointcut("execution(#com.example.controller.WelcomeController * *(#com.example.controller.Encrypted (*), ..)) && args(password)")
public void encryptedMethod(String password) {}
#Around("encryptedMethod(password)")
public Object encryptedMethod(ProceedingJoinPoint pjp, String password) throws Throwable {
// process the password string ...
return pjp.proceed();
}
}
Check more on the AOP with Spring here -- https://www.baeldung.com/spring-aop
Related
#RestController()
#RequestMapping(path = "/users")
public class UserController {
#GetMapping()
public #ResponseBody Page<User> getAllUsers(#RequestParam Integer pageSize, UserRequest userRequest) {
//TODO: some implementation
}}
public class UserRequest{
public String name;
public String age;
}
send the request with invalid parameter, like localhost:8800/users?name1=1234, I want to return error. but in fact it ignore the invalid parameter name1.
I tried to add the user defined annotation on the method parameter and on the class , codes like below
#RestController()
#RequestMapping(path = "/users")
#Validated
public class UserController {
#GetMapping()
public #ResponseBody Page<User> getAllUsers(#RequestParam #Validated Integer pageSize, #Validated UserRequest userRequest} {
//TODO: some implementation
}
}
But it does not working.
I think it is happened because framework has ignore the invalid parameter before the method was called.
where did framework handle the url and how can I do to make it return error instead of ignore?
You can reject parameters that are not valid. You can do so in a HandlerInterceptor class.
Reference: Rejecting GET requests with additional query params
In addition to what is done in the above reference, in your addInterceptors, you can specify the path that is intercepted.
Like this:
#Configuration
public class WebConfig implements WebMvcConfigurer {
private String USERS_PATH = "/users";
// If you want to cover all endpoints in your controller
// private String USERS_PATH = List.of("/users", "/users/**");
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new FooHandlerInterceptor()).addPathPatterns(USERS_PATH);
}
}
I want to validate the password field length before it's hashed.
Model class:
#Entity
#Table(name = "users")
public class UserInfo {
/* other code */
#NotBlank(message = "Password is required")
#Size(min = 6, message = "Password should have min 6 characters")
private String password;
/* other code */
}
Controller file simply calls the service method.
Service class:
#Component
public class UserInfoServiceImpl implements UserInfoService {
#Autowired
private UserInfoRepository userInfoRepository;
public UserInfo register(UserRegisterRequest request) {
UserInfo user = new UserInfo();
user.setFirstName(request.getFirstName());
user.setLastName(request.getLastName());
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
// Password hashing
user.setPassword(new BCryptPasswordEncoder().encode(request.getPassword()));
user.setIsActive(0);
user.setStatus(1);
return userInfoRepository.save(user);
}
}
I suppose the password is validated after it's hashed in this line:
user.setPassword(new BCryptPasswordEncoder().encode(request.getPassword()));
How can I validate this password before hashing and saving?
Any help would be appreciated. Thanks in advance.
You can validate the input of any Spring bean. In order to to this, you use a combination of the #Validated and #Valid annotations, like this:
#Component
#Validated
public class UserInfoServiceImpl implements UserInfoService {
#Autowired
private UserInfoRepository userInfoRepository;
public UserInfo register(#Valid UserRegisterRequest request) {
UserInfo user = new UserInfo();
user.setFirstName(request.getFirstName());
user.setLastName(request.getLastName());
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
// Password hashing
user.setPassword(new BCryptPasswordEncoder().encode(request.getPassword()));
user.setIsActive(0);
user.setStatus(1);
return userInfoRepository.save(user);
}
}
If you want a better control, you can validate programmatically:
#Component
public class UserInfoServiceImpl implements UserInfoService {
#Autowired
private UserInfoRepository userInfoRepository;
public UserInfo register(UserRegisterRequest request) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<UserRegisterRequest> violations = validator.validate(input);
if (!violations.isEmpty()) {
// Do something on invalid input;
}
UserInfo user = new UserInfo();
user.setFirstName(request.getFirstName());
user.setLastName(request.getLastName());
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
// Password hashing
user.setPassword(new BCryptPasswordEncoder().encode(request.getPassword()));
user.setIsActive(0);
user.setStatus(1);
return userInfoRepository.save(user);
}
}
Alternatively, a pre-configured validator instance can be injected like this:
#Autowired
Validator validator;
For these to work, you need spring-boot-starter-validation in your Maven/Gradle config file.
You can use #Valid.
#RestController
public class UserController {
#PostMapping("/users")
ResponseEntity<String> register(#Valid #RequestBody User user) {
userService.register(user);
}
}
When Spring Boot finds an argument annotated with #Valid, it automatically bootstraps the default JSR 380 implementation — Hibernate Validator — and validates the argument.
When the target argument fails to pass the validation, Spring Boot throws a MethodArgumentNotValidException exception.
You can handle and customize message using #ExceptionHandler
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return errors;
}
Reference
I have a flag DISABLE_FLAG and I want to use it to control multiple specific APIs in different controllers.
#RestController
public final class Controller1 {
#RequestMapping(value = "/foo1", method = RequestMethod.POST)
public String foo1()
}
#RestController
public final class Controller2 {
#RequestMapping(value = "/foo2", method = RequestMethod.POST)
public String foo2()
}
I can use an interceptor to handle all the urls. Is there a easy way to do that like annotation?
You could use AOP to do something like that.
Create your own annotation...
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface Maybe { }
and corresponding aspect...
#Aspect
public class MaybeAspect {
#Pointcut("#annotation(com.example.Maybe)")
public void callMeMaybe() {}
#Around("callMeMaybe()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// do your logic here..
if(DISABLE_FOO) {
// do nothing ? throw exception?
// return null;
throw new IllegalStateException();
} else {
// process the request normally
return joinPoint.proceed();
}
}
}
I don't think there is direct way to disable a constructed request mapping but We can disable API in many ways with some condition.
Here is the 2 ways disabling by spring profile or JVM properties.
public class SampleController {
#Autowired
Environment env;
#RequestMapping(value = "/foo", method = RequestMethod.POST)
public String foo(HttpServletResponse response) {
// Using profile
if (env.acceptsProfiles("staging")) {
response.setStatus(404);
return "";
}
// Using JVM options
if("true".equals(System.getProperty("DISABLE_FOO"))) {
response.setStatus(404);
return "";
}
return "";
}
}
If you are thinking futuristic solution using cloud config is the best approach. https://spring.io/guides/gs/centralized-configuration/
Using Conditional components
This allows to build bean with conditions, if the condition failed on startup, the entire component will never be built. Group all your optional request mapping to new controller and add conditional annotation
#Conditional(ConditionalController.class)
public class SampleController {
#Autowired
Environment env;
#RequestMapping(value = "/foo", method = RequestMethod.POST)
public String foo(HttpServletResponse response) {
return "";
}
public static class ConditionalController implements Condition {
#Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().acceptsProfiles("staging"); // Or whatever condition
}
}
}
You can solve this with annotations by utilizing spring profiles. You define two profiles one for enabled flag and another profile for the disabled flag. Your example would look like this:
#Profile("DISABLED_FLAG")
#RestController
public final class Controller1 {
#RequestMapping(value = "/foo1", method = RequestMethod.POST)
public String foo1()
}
#Profile("ENABLED_FLAG")
#RestController
public final class Controller2 {
#RequestMapping(value = "/foo2", method = RequestMethod.POST)
public String foo2()
}
Here is the link to the spring framework documentation for this feature: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Profile.html
I did it as follows :
#Retention(RUNTIME)
#Target(ElementType.METHOD)
public #interface DisableApiControl {
}
This class is my customization statement. After could use AOP :
for AbstractBaseServiceImpl :
public abstract class AbstractBaseServiceImpl {
private static boolean disableCheck = false;
public void setDisableChecker(boolean checkParameter) {
disableCheck = checkParameter;
}
public boolean getDisableChecker() {
return disableCheck;
}
}
NOTE : The above class has been prepared to provide a dynamic structure.
#Aspect
#Component
public class DisableApiControlAspect extends AbstractBaseServiceImpl {
#Autowired
private HttpServletResponse httpServletResponse;
#Pointcut(" #annotation(disableMe)")
protected void disabledMethods(DisableApiControl disableMe) {
// comment line
}
#Around("disabledMethods(disableMe)")
public Object dontRun(ProceedingJoinPoint joinPoint, DisableApiControl disableMe) throws Throwable {
if (getDisableChecker()) {
httpServletResponse.sendError(HttpStatus.NOT_FOUND.value(), "Not found");
return null;
} else {
return joinPoint.proceed();
}
}
}
checker parameter added global at this point. The rest will be easier when the value is given as true / false when needed.
#GetMapping("/map")
#DisableApiControl
public List<?> stateMachineFindMap() {
return new ArrayList<>;
}
I have a controller:
#Authorised(id = "{personId}")
#RequestMapping(value = {"{personId}"}, method = GET)
public void test(#PathVariable PersonId personId) {
System.out.println(personId); //gets personId
}
Annotation:
#Target({ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface Authorised {
String id() default "";
}
Pointcut:
#Pointcut("#annotation(Authorised)")
private void AuthorisedMethod() {}
And the method that has to get {personId} value not string "{personId}":
#Before("AuthorisedMethod()")
public void checkIfIsCurrentlyAuthenticated(JoinPoint joinPoint) throws NoSuchMethodException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = signature.getMethod().getName();
Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
Parameter[] parameters = signature.getMethod().getParameters();
Authorised annotations = joinPoint.getTarget().getClass().getMethod(methodName, parameterTypes).getAnnotation(Authorised.class);
String id = annotations.id();
System.out.println(id); // prints: "{personId}"
// do the chekcing
throw new UnauthenticatedUserException();
}
Can it be achieved and how?
UPDATE: But what if method argument parameter number don't match with the pointcut args()? I mean that what if specific method has parameter #PathVariable PersonId personId and few more, but poincut needs to know only PersonId personId?
Like #statut said you have to write args() like that: args(personId,..)
You can modify #Before() annotation to have PersonId value and pass this value to aspect, for example
#Before("AuthorisedMethod() && args(personId)")
public void checkIfIsCurrentlyAuthenticated(JoinPoint joinPoint, PersonId personId) throws NoSuchMethodException {}
To test it I had the following Aspect:
#Aspect
#Component
public class SomeAspect {
#Pointcut("#annotation(Authorised)")
private void AuthorisedMethod() {
}
#Before("AuthorisedMethod() && args(personId)")
public void checkIfIsCurrentlyAuthenticated(JoinPoint joinPoint, PersonId personId) throws NoSuchMethodException {
System.out.println("aspect " + personId.getId());
}
}
Configuration class:
#Configuration
#ComponentScan(basePackages = {"test"})
#EnableAspectJAutoProxy(proxyTargetClass = true)
public class Config {
}
Test component:
#Component
public class Test {
#Authorised(id = "{personId}")
public void test(PersonId personId) {
System.out.println("component " + personId.getId()); //gets personId
}
}
And testNG's runner:
#ContextConfiguration(classes = Config.class)
public class TestRunner extends AbstractTestNGSpringContextTests {
#Autowired
test.Test test;
#Test
public void testName() {
test.test(new PersonId("id"));
}
}
When I run it, I get printed "aspect id" from aspect and "component id" from invoked method.
You can also get the value of the PathVariable in RequestMapping URL using HandlerInterceptor if that is possible for you.
Write a Handler class that intercepts this Request.
public class AuthorisedHandler extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (!isResourceHandler(handler) && (handler instanceof HandlerMethod)) {
HandlerMethod hm = (HandlerMethod) handler;
Method method = hm.getMethod();
Authorised authAnnotation = method.getAnnotation(Authorised.class);
if (authAnnotation != null) {
String personId = getPersonId(request);
//Do all your validations Here
}
}
return true;
}
#SuppressWarnings("rawtypes")
private String getPersonId(HttpServletRequest request) throws IOException {
String personId = request.getParameter("personId");
if(personId == null || personId.equals("")){
Map pathVariables = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
personId = (String) pathVariables.get("personId");
}
return personId;
}
private boolean isResourceHandler(Object handler) {
return handler instanceof ResourceHttpRequestHandler;
}
}
And you must configure this Handler bean in spring config xml or Spring Java Config.
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.**.AuthorisedHandler" />
</mvc:interceptor>
</mvc:interceptors>
Now, all the requests will go through this Interceptor. Only which are annotated with #Authorised will go through.
I'm trying to add a request parameter with a default value - however I'd like that default value to be the logged in user's name.
I have a method getUsername() which returns the current user's name but I can't set the value of an annotation to a method call (or a class attribute). Here's my method:
#RequestMapping(method = RequestMethod.GET)
public #ResponseBody ResponseEntity<WebUser> getUser(
#RequestParam(value = "username", defaultValue = getUsername()) String username) {
return ResponseEntity.ok(service.getUser(getUsername(), username.toLowerCase()));
}
I can make the RequestParam not required and populate it if null - but this doesn't feel very elegant (or spring-ish). Is there another way to accomplish this?
As suggested by fateddy, the easiest way to do this is by implementing a HandlerMethodArgumentResolver.
public class UsernameHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType().equals(Username.class);
}
#Override
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {
String username = nativeWebRequest.getParameter("username");
if (username == null && nativeWebRequest.getUserPrincipal() != null) {
username = nativeWebRequest.getUserPrincipal().getName();
}
return new Username(username);
}
}
This requires a simple username class:
public class Username {
private String username;
public Username(String username) {
this.username = username;
}
public String getValue() {
return this.username;
}
}
as well as an annotation
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
public #interface UserRequest {}
In order to get this configured properly this requires a very minor change to the WebMvcConfigurerAdapter:
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new UsernameHandlerMethodArgumentResolver());
}
And that's it. Good to go. Now we can be simply drop the argument into a controller endpoint:
#RequestMapping(method = RequestMethod.GET)
public #ResponseBody ResponseEntity<WebUser> getUser(#UserRequest Username username) {
return ResponseEntity.ok(service.getUser(username, username.toLowerCase()));
}
I am not sure whether you can call method in annotation or not,but you can get user name value just before the #RequestMapping call execution by something like below.
I think this can be achieved using #ModelAttribute.
#ModelAttribute("username")
public String getUserName() {
String name = "XYZ";
return name;
}
And in your #RequestMapping do like below.
#RequestMapping(method = RequestMethod.GET)
public #ResponseBody ResponseEntity<WebUser> getUser(
#ModelAttribute("username") String username) {
return ResponseEntity.ok(service.getUser(username, username.toLowerCase()));
}
Further reading