I am very confused about this architecture. I am not even sure is it possible.
I have more than 10 microservises and a API Gateway. I want to add authentication and authorization to this system. One of this services is authentication-server and it has
an endpoint which is /signin
#PostMapping(value = "/signin")
public UserLoginResponse login(#Valid #RequestBody UserLoginRequest userLoginRequest) {
return authService.login(userLoginRequest);
}
public class UserLoginResponse {
private String accessToken; //accessToken is jwt token and it has ROLE field.
}
public class UserLoginRequest {
private String username;
private String password;
}
Here is the confusing part for me: Right now gateway creates code duplication. When I add to a new endpoint, I need to add almost same controller/service/models to API Gateway.
For example:
Lets say microservice A has /product endpoint, these are (veeery roughly) the classes I should have
// Controller
class ProductController {
#GetMapping("/product/{id}")
public ProductResponse getProduct(#PathVariable String id) {
return productService.getProduct(id)
}
}
// Service
class ProductService {
public getProduct(String id){
return productRepository.get(id);
}
}
// Response DTO
class ProductResponse(){
private String id;
private String name;
}
Our team also has implemented classes in the gateway.
//Controller has authorization with #PreAuthorize annotation.
#RestController
#PreAuthorize("hasAnyRole('USER', 'ADMIN')")
class ProductController {
#GetMapping("/product/{id}")
public ProductResponse getProduct(PathVariable String id) {
return productService.getProduct(id)
}
}
// Service
class ProductService {
private final ClientApi productClientApi;
public ProductService(ClientApi productClientApi) {
this.productClientApi = productClientApi;
}
public getProduct(String id){
return productClientApi.getProduct(id);
}
}
//This is feign client. It makes http requests to product-api
#FeignClient(
"product-api",
url = "\${product-api.base-url}",
configuration = [FeignClientConfiguration::class]
)
interface ClientApi(){
#GetMapping( value = {"product/{id}"}, consumes = {"application/json"} )
ProductResponse getProduct(#PathVariable String id);
}
// Response DTO
class ProductResponse(){
private String id;
private String name;
}
When a request comes to /product its jwt token controlled here and if it has proper permission, it goes to the service layer,
Service layer makes request to product-api(which is microservice A)
returns the response
Question: There should be easier way. Every new endpoint in the services costs us code duplication in Gateway. I think I want just routing. Whatever comes to gateway directly should be routed to services and it should still has the authentication/authorization responsibility. I know that I can do that as below with spring-cloud-gateway but I couldn't figure out how can i do that with authentication and authorization. Can anyone explain me that am i thinking wrong ?
spring:
application:
name: "API-GATEWAY"
cloud:
gateway:
routes:
- id: product-service
uri: 'http://product-url:8083'
predicates:
- Path=/product/**
Related
I'm running a quarkus setup with keycloak. To start of I am trying to implement the tutorial into my code. I have to note here that i didn't copy the realm that is used. The reason for this is that we received a realm from our tutors.
This is the code I am trying to implement. The problem with is that it doesn't return the user.
#Path("/api/users")
public class UserResource {
#Inject
SecurityIdentity identity;
#GET
#Path("/me")
#NoCache
public User me() {
return new User(identity);
}
public static class User {
private final String userName;
User(SecurityIdentity identity) {
this.userName = identity.getPrincipal().getName();
}
public String getUserName() {
return userName;
}
}
}
When I enabled policy enforcer, I wasn't even able to login, all request became 403 or 401.
quarkus.keycloak.policy-enforcer.enable=true
How can we solve this issue?
Another question would be if it is possible to retrieve some sort of userId from keycloak?
I have a rest api which has role based access to its http POST method. It uses spring-security to access api only for authorized users.
I have 2 questions,
how external client can pass request body and user object (#AuthenticationPrincipal) to make this api call
How can I write junit to test below piece of code,
#PreAuthorize("hasAuthority('ADMIN')")
#PostMapping("/api/access/submit")
public ResponseEntity<OrderAdminResponse> create(#RequestBody OrderAdminRequest orderAdminSubmitRequest,#AuthenticationPrincipal UserObject user)
{
return ResponseEntity.accepted().body(orderService.submit(orderAdminSubmitRequest));
}
My User Object is below,
<code>
public class UserObject {
private final String name;
private final String id;
private final String email;
private UserObject(String name, int id, String email){
this.name = name; this.id = id; this.email = email
}
public Collection<String> getRoles() {
return
(Collection)this.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toList());
}
public boolean isUserInRole(String role) {
return this.getAuthorities().stream().anyMatch((a) -> {
return a.getAuthority().equals(role);
})
}
}
</code>
The controller will automatically populate the Authentication Principal, when you make a request with an authenticated user.
For example, if you are using HTTP basic authentication to secure your endpoint, then the principal will be populated from the Authorization header. Your request body remains the same regardless of whether you are extracting the principal or not.
An easy way to test your controller is to use the support provided by Spring Security.
If you are using MockMvc, one option is to use a post processor to call your endpoint with different types of users.
this.mvc.perform(post("/api/access/submit")
.content("...")
.with(user("user").roles("USER")))
.andExpect(status().isUnauthorized());
this.mvc.perform(post("/api/access/submit")
.content("...")
.with(user("admin").roles("ADMIN")))
.andExpect(status().isOk());
You can learn more about Spring Security test support in the documentation here.
I have a spring mvc project, i want to convert spring rest api project.
Here my sample codes;
My Controller
#Controller
#RequestMapping("/student")
public class StudentController {
#Autowired
private StudentService studentService;
#Autowired
private SchoolService schoolService;
#GetMapping("/list")
public String ListStudents(Model model) {
List<Student> theStudent = studentService.getStudents();
model.addAttribute("students", theStudent);
return "List-student";
}
#GetMapping("/addNewStudent")
public String addNewStudent(Model model) {
Student theStudent = new Student();
model.addAttribute("student", theStudent);
List<School> theSchool = schoolService.getSchools();
model.addAttribute("schools", theSchool);
return "student-form";
}
#PostMapping("/saveStudent")
public String saveStudent(#ModelAttribute("student") Student theStudent) {
studentService.saveStudent(theStudent);
return "redirect:/student/list";
}
#GetMapping("/showFormforUpdate")
public String showFormForUpdate(#RequestParam("studentID") int theId, Model model) {
Student theStudent = studentService.getStudent(theId);
model.addAttribute(theStudent);
List<School> theSchool = schoolService.getSchools();
model.addAttribute("schools", theSchool);
return "student-form";
}
#GetMapping("/deleteStudent")
public String deleteStudent(#RequestParam("studentID") int theId, Model model) {
studentService.deleteStudent(theId);
return "redirect:/student/list";
}
}
My DAOImpl class
#Repository
public class StudenDAOImpl implements StudentDAO {
#Autowired
private SessionFactory sessionFactory;
#Override
#Transactional
public List<Student> getStudents() {
Session currentSession=sessionFactory.getCurrentSession();
Query<Student> theQuery = currentSession.createQuery("from Student", Student.class);
List<Student> students=theQuery.getResultList();
return students;
}
#Override
public void saveStudent(Student theStudent) {
Session currentSession=sessionFactory.getCurrentSession();
currentSession.saveOrUpdate(theStudent);
}
#Override
public Student getStudent(int theId) {
Session currentSession=sessionFactory.getCurrentSession();
Student theStudent=currentSession.get(Student.class, theId);
return theStudent;
}
#Override
public void deleteStudent(int theId) {
Session currentSession=sessionFactory.getCurrentSession();
Query theQuery=currentSession.createQuery("delete from Student where id=:studentID");
theQuery.setParameter("studentID", theId);
theQuery.executeUpdate();
}
}
Actually i know the places i need to change. But i don't know very well spring Rest Api. In my code i send many attributes in my view(jsp file) and i catch this attributes here . But RestApi does not have views. I need to delete attribute functions. But what can i add instead of attribute functions? I am new the RestApi please help me what should i do?
welcome to stack overflow.
Rest APIs work like this:
You expose an endpoint and a verb (for example GET at /students)
Some client calls your endpoint (makes a call to the server that holds your app)
Your server delegates the call to a controller function ( a function with a #GetMapping("/students") for example )
The controller function sends a response to the client (With spring, your method returns an object or a ResponseEntity)
REST APIs receive requests , process said request (and request data if present) and tipically return some data with a status code that indicates if the operation was successful.
With spring you do something like this:
#GetMapping("/students")
ResponseEntity<List<Student>> listStudents() {
List<Student> stds = getStudents(); // call some service or DB
return new ResponseEntity<>(stds, HttpStatus.OK);
}
When you make a GET request to /students, spring will delegate the handling of the request to the listStudents method which will get the students and return data.
REST APIs tipically work with JSON, so the list of students you'll be returning will be serialized into a json list.
If you want to customize the student json structure you can use Jackson:
public class Student {
#JsonProperty("cool_name") private String name;
// getters and setter
}
A rest api does not work with views or JSP. They tipically work with http requests and responses.
If you're working with spring mvc instead of spring boot, check this article:
https://viralpatel.net/blogs/spring-4-mvc-rest-example-json/
If you can use spring boot (which I highly recommend) , check this:
https://spring.io/guides/gs/rest-service/
You should also annotate your REST controllers with #RestController for them to automatically handle rest calls.
Hope this helps and welcome to stack overflow
I have a bunch of intermediate and core services within my application. All services are Spring Boot and using Netflix Library. When a user requests information, the request will/might pass other services in the chain eg:
Client <-> Zuul <-> Service B <-> Service A
I have configured all services (A and B) to be ResourceServer so that every access needs to be authenticated. When requesting an access token (From a Spring Security Server) and use it to request information directly from Service A, everything works fine. When I use the same token to access information from Service B (which needs Service A down the line) I get an "HTTP 401: Full authentification is required" error. Service B uses a FeignClient to call Service A.
After some debugging, I found out, that the Authorization-Header is not passed from Service B to Service A. Service B checks the token itself correctly, grants access to the method and tries to perform the request of Service A.
I tried a RequestInterceptor but without any success (Error "Scope 'request' is not active for the current thread")
#Component
public class OAuth2FeignRequestInterceptor implements RequestInterceptor {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_TOKEN_TYPE = "Bearer";
private final OAuth2ClientContext oauth2ClientContext;
public OAuth2FeignRequestInterceptor(OAuth2ClientContext oauth2ClientContext) {
Assert.notNull(oauth2ClientContext, "Context can not be null");
this.oauth2ClientContext = oauth2ClientContext;
}
#Override
public void apply(RequestTemplate template) {
if (template.headers().containsKey(AUTHORIZATION_HEADER)) {
...
} else if (oauth2ClientContext.getAccessTokenRequest().getExistingToken() == null) {
...
} else {
template.header(AUTHORIZATION_HEADER, String.format("%s %s", BEARER_TOKEN_TYPE,
oauth2ClientContext.getAccessTokenRequest().getExistingToken().toString()));
}
}
}
This is an example proxy function that uses the FeignClient:
#Autowired
private CategoryClient cat;
#HystrixCommand(fallbackMethod = "getAllFallback", commandProperties = {#HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "2") })
#GetMapping("/category")
public ResponseEntity<List<Category>> getAll() {
try {
ResponseEntity<List<Category>> categories = this.cat.getAll();
...
return categories;
} catch(Exception e) {
...
}
}
Is there any working solution to pass the Authorization-Header from the proxy function to the FeignClient so that Service A will receive the header and can do its own auth check with it?
Found a working solution. I still don't know if this is the "best" way to do it and if anyone got a better solution I'd be happy if you share it. But for now, this is working as expected:
#Bean
public RequestInterceptor requestTokenBearerInterceptor() {
return new RequestInterceptor() {
#Override
public void apply(RequestTemplate requestTemplate) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
requestTemplate.header("Authorization", "Bearer " + details.getTokenValue());
}
};
}
I'm using Spring boot 1.4.0, Consider below code in a #RestController, what I expect is, the server side will receive a http body with form_urlencoded content type, but unfortunately it demands me a query parameter type with email and token. What's the problem here and how to fix?
#DeleteMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
#ResponseStatus(HttpStatus.NO_CONTENT)
public void removeAdmin(#RequestParam(value = "email") String email, #RequestParam(value = "token") String token) {
//...
}
#DeleteMapping is only a convenience extension the provides #RequestMapping(method=DELETE) It will not handle request paramters. You will still have to map those in the controllers method signature if you need the data to perform the work.
Since you want a body, You could create an object and mark it as #RequestBody:
public class DeleteBody {
public String email;
public String token;
}
public void removeAdmin(#RequestBody DeleteBody deleteBody) {
...
}