I'm trying to understand Spring Security and I'm wondering about creating my own annotations with authorities I've created. I've got something like this:
#PreAuthorize("hasAuthority('STANDARD')")
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface StandardRole {
}
#PreAuthorize("hasAuthority('ADMIN')")
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface AdminRole {
}
and actually it looks like:
#AdminRole
#StandardRole
#GetMapping(path = "user", produces = "application/json")
public ResponseEntity<User> getUser(#RequestParam String login) {
...
}
but only first annotation works, second one is ommited. I want to do something like #AllowRoles() annotation, for example #Allow({UserType.ADMIN, UserType.STANDARD}) or #Allow({UserType.ADMIN}).
How can I do this? Thanks.
It's a pity to force the door open.
I used jsr250 annotation in my SecurityConfig class:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
#RolesAllowed in my controller:
#RolesAllowed({UserType.TYPE_1, UserType.TYPE_2})
#GetMapping(path = "user", produces = "application/json")
public ResponseEntity<User> getUser() {
Finally, at my CustomDetails implementing UserDetails:
private static final String PREFIX = "ROLE_";
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(
new SimpleGrantedAuthority(
PREFIX + user.getUserType()));
}
I forgot about "ROLE_" prefix. Code is much cleaner.
Related
My question is related to bean validation. Apparently SpringBoot comes with two different validation mechanisms, one JSR-303 compilant (in javax.validation package) and the other provided by Spring framework (in org.springframework.validation package).
While it is easy to enforce JSR-303 validation via #Validated and #Valid annotations, I couldn't find the proper way for Spring's ones.
Take the following User bean.
#Entity
public class User {
#Column
#Id
private String id;
#Column(unique = true)
#Username
private String username;
// [...]
}
Where #Username constraint is defined as follows. Basically, it's just a composition of #Pattern and #Size costraints.
#Constraint(validatedBy = {})
#Documented
#Pattern(regexp = "[A-Za-z0-9_]+")
#Retention(RUNTIME)
#Size(max = 24, min = 3)
#Target(FIELD)
public #interface Username {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
User beans are stored in a repository named UserRepository, defined as follows.
#Repository
public interface UserRepository extends CrudRepository<User, String>, JpaSpecificationExecutor<User> {
}
To access the repository I wrote a service, shown below.
#Transactional
#Service
public class UserService {
#Autowired
private UserRepository userRepository;
#Validated
public void create(#Valid User user) {
userRepository.save(user);
}
}
Until now, things are pretty neat and everything works. With just a couple of annotations I've achieved everything.
Now, I have this other validator (not JSR-303).
// [...]
import org.springframework.validation.Validator;
#Component
public class UniqueUsernameValidator implements Validator {
#Autowired
private UserRepository userRepository;
#Override
public boolean supports(Class<?> clazz) {
return User.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
User user = (User) target;
if (userRepository.count(where(usernameIsEqualTo(user.getUsername()))) > 0) {
errors.rejectValue("username", "username.exists");
}
}
}
Ideally, I would like to have it enforced via the same #Validated and #Valid annotations but until now I've been unlucky. I guess it is definitely possible given that the documentation of the class org.springframework.validation.Validator says
This interface is totally divorced from any infrastructure or context; that is to say it is not coupled to validating only objects in the web tier, the data-access tier, or the whatever-tier. As such it is amenable to being used in any layer of an application, and supports the encapsulation of validation logic as a first-class citizen in its own right.
#Valid is a JSR-303 bean validation annotation, and it won't call your UniqueUsernameValidator Spring Validator.
If you want to do that in your service, you need to invoke it manually:
public void create(#Valid User user) {
Errors errors = new BeanPropertyBindingResult(user, "user");
// Validator component has been previously injected into the service.
uniqueUsernameValidator.validate(user, errors);
if (errors.hasErrors()) {
throw new RuntimeException(errors.toString());
}
userRepository.save(user);
}
I know how to validate #PathVariable from https://stackoverflow.com/a/35404423/4800811
and it worked as expected with standard annotations but not with the customized one using a Repository bean. Maybe the bean is not initialized and I end up with NullPointerException when accessing the end point has #PathVariable validated. So how to get that work?
My Controller:
#RestController
#Validated
public class CustomerGroupController {
#PutMapping(value = "/deactive/{id}")
public HttpEntity<UpdateResult> deactive(#PathVariable #CustomerGroupEmpty String id) {
}
}
My custom validator:
public class CustomerGroupEmptyValidator implements ConstraintValidator<CustomerGroupEmpty, String>{
#Autowired
private CustomerRepository repository;
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// NullPointerException here (repository == null)
if (value!=null && !repository.existsByCustomerGroup(value)) {
return false;
}
return true;
}
}
My Custom Annotation:
#Documented
#Constraint(validatedBy = CustomerGroupEmptyValidator.class)
#Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
public #interface CustomerGroupEmpty {
String message() default "The customer group is not empty.";
Class<?>[] groups() default {};
Class<? extends Payload> [] payload() default {};
}
code in this post is correct, only mistake is that validator need to override initialize method as well. Probably user123 incorrect configure repository bean, the simply way to check this is define it manually in configuration class
I can define a Spring ControllerAdvice that is selectively used by a subset of controllers using a custom annotation:
#RestController
#UseAdviceA
#RequestMapping("/myapi")
class ApiController {
...
}
#ControllerAdvice(annotations = UseAdviceA.class)
class AdviceA {
...
}
But is it possible to pass in an attribute via the custom annotation where the advice class can pick up from the annotation? For e.g.:
#RestController
#UseAdviceA("my.value")
#RequestMapping("/myapi")
class ApiController {
...
}
#ControllerAdvice(annotations = UseAdviceA.class)
class AdviceA {
// Some way to get the string "myvalue" from the instance of UseAdviceA
...
}
Any other way to achieve the same outcome, which is to be able to define a custom configuration at the Controller method which can be passed to the ControllerAdvice would be much appreciated too.
Here is a solution.
Given
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface UseAdviceA {
public String myValue();
}
Controller
#RestController
#UseAdviceA(myValue = "ApiController")
#RequestMapping("/myapi")
class ApiController {
...
}
Your Controller Advice should be like
#ControllerAdvice(annotations = {UseAdviceA.class})
class AdviceA {
#ExceptionHandler({SomeException.class})
public ResponseEntity<String> handleSomeException(SomeException pe, HandlerMethod handlerMethod) {
String value = handlerMethod.getMethod().getDeclaringClass().getAnnotation(UseAdviceA.class).myValue();
//value will be ApiController
return new ResponseEntity<>("SomeString", HttpStatus.BAD_REQUEST);
}
I have a custome annotation:
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Controller
#RequestMapping("auth/firebase")
public #interface FirebaseAuth {
#AliasFor(annotation = RequestMapping.class, attribute = "path")
String[] path() default {};
}
I want to achive, that a controller which is annotated with:
#FirebaseAuth(path = "user")
public class UserController {
Will end up with a path like this: auth/firebase/user, to secure the resource
How is it possible to extend the path with auth/firebase before "user" with an annotation (not with a superclass)?
Thanks! :)
In my controller, the following use of #GetMapping works:
#GetMapping(value = "/new")
public String newEssay(){
return "articles/essay_new";
}
But it doesn't work like this:
#GetMapping(value = "/essays/{essayId: [0-9]+}")
//#RequestMapping(value = "/essays/{essayId:[0-9]+}", method = RequestMethod.GET)
public String getEssay(Model model,
#PathVariable("essayId") long essayId) throws NoFindException, ForBiddenException, ParseException {
JsEssay jsEssay = jsBiz.get(JsEssay.class, essayId);
model.addAttribute("jsEssay", jsEssay);
return "articles/essay";
}
I tried it with Spring 4.3.3 and 5.0.0.M5.
Config:
#Configuration
#ComponentScan( basePackages = {"me.freezehome.blog"},
excludeFilters = {
#ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)
}
)
public class RootConfig {
}
#Configuration
#EnableWebMvc
#Import({WebSecurityConfig.class})
public class WebConfig extends WebMvcConfigurerAdapter{
#Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping(){
return new RequestMappingHandlerMapping();
}
#Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(){
return new RequestMappingHandlerAdapter();
}
}
Google results:
Add support for #GetMapping, #PostMapping etc. introduced in Spring 4.3 in ControllerLinkBuilder #471
GetMapping and PostMapping annotations Ask
github source: lbfreeze-blog-develop
Please remove the space after essayId:
Also, you don't need to write value = for #GetMapping.