Custom Header not added through apache cxf OutInterceptor with spring - spring

I have been struggling all day to have a custom SOAP request using spring application context and apache cxf and spring.
My Interceptor class looks like below
public class HttpHeaderInterceptor extends AbstractPhaseInterceptor<Message> {
public HttpHeaderInterceptor() {
super(Phase.SETUP);
}
#Override
public void handleMessage(Message message) throws Fault {
Map<String, List<String>> ietHeaders = new HashMap<String,List<String>>();
List<String> headerItems = new LinkedList<>();
ietHeaders.put("CustomHeader", Arrays.<String>asList("myheader"));
message.put(Message.PROTOCOL_HEADERS, ietHeaders);
}
}
WHen I check with Charlesproxy it's just the normal request. I am sure I am doing something wrong. At debug time , I can step into handleMessage method but nothing changes. The rest of the code snipet is available on pastie.org
Can anyone point out the oversight?
Thanks

Change Interceptor to SoapPreProtocolOutInterceptor. For details refer link
Hence modify the class as below.
public class HttpHeaderInterceptor extends SoapPreProtocolOutInterceptor {
public void handleMessage(SoapMessage message) throws Fault {
Map<String, List<String>> ietHeaders = new HashMap<String, List<String>>();
List<String> headerItems = new LinkedList<String>();
headerItems.add("h1");
headerItems.add("h2");
headerItems.add("h3");
ietHeaders.put("CustomHeader", headerItems);
message.put(Message.PROTOCOL_HEADERS, ietHeaders);
}
}
Modify your cxf-bean.xml to include interceptor
<jaxws:outInterceptors>
<bean class="com.kp.swasthik.soap.interceptor.HttpHeaderInterceptor" />
</jaxws:outInterceptors>
The output would be as below.
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
CustomHeader: h1,h2,h3
Content-Type: text/xml;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 12 Aug 2014 11:17:57 GMT

Related

Add Basic Authorization HTTP Headers to SOAP Request with Spring-WS

I have been trying to consume a SOAP service using a Spring application, but I've been absolutely stuck for a while. I expect the solution will be very simple and I'm hoping to be pointed in the right direction.
I am able to successfully make requests via SoapUI. I simply loaded the .WSDL file, selected the method to use, and added the username and password with basic authorization from the 'Auth' menu in SoapUI.
In the Spring application, I've gotten to the point of getting a 401 error, so I believe it's almost there. I referenced these two nice examples below to first add the username and password, and secondly to log the HTTP headers to verify that they are populated correctly.
https://codenotfound.com/spring-ws-log-client-server-http-headers.html
Setting a custom HTTP header dynamically with Spring-WS client
However, neither setting the 'usernamePasswordCredentials', nor setting the connection's request header seems to have any effect. I've also confirmed that the XML body is correct by testing the logged output in SoapUI. So I believe it's just an authorization issue.
Bean/Configuration Class:
#Bean
public Jaxb2Marshaller marshaller() {
System.out.println("BEAN CREATED: MARSHALLER...");
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setContextPath("..."); // omitted for example
return marshaller;
}
#Bean
public UsernamePasswordCredentials usernamePasswordCredentials() {
return new UsernamePasswordCredentials("testu", "test");
}
#Bean
#DependsOn({"usernamePasswordCredentials"})
public HttpComponentsMessageSender httpComponentsMessageSender(UsernamePasswordCredentials usernamePasswordCredentials) {
HttpComponentsMessageSender httpComponentsMessageSender = new HttpComponentsMessageSender();
httpComponentsMessageSender.setCredentials(usernamePasswordCredentials);
return httpComponentsMessageSender;
}
#Bean
#DependsOn({"marshaller"})
public TicketDetailsClient ticketDetailsClient(Jaxb2Marshaller marshaller) {
System.out.println("BEAN CREATED: TICKETDETAILSCLIENT...");
TicketDetailsClient ticketDetailsClient = new TicketDetailsClient();
ticketDetailsClient.setDefaultUri("..."); // omitted for example
ticketDetailsClient.setMarshaller(marshaller);
ticketDetailsClient.setUnmarshaller(marshaller);
return ticketDetailsClient;
}
Bean Method:
public GetTicketDetailsResponse getTicketDetails(long definitionId, long itemId) {
ObjectFactory of = new ObjectFactory();
this.template.setInterceptors(new ClientInterceptor[] {new LogHttpHeaderClientInterceptor()});
GetItemDetailsRequest itemDetailsRequest = new GetItemDetailsRequest();
itemDetailsRequest.setItemDefinitionId(definitionId);
itemDetailsRequest.setItemId(itemId);
GetTicketDetails getTicketDetails = new GetTicketDetails();
getTicketDetails.setGetItemDetailsRequest(itemDetailsRequest);
JAXBElement<GetTicketDetails> elGetTicketDetails = of.createGetTicketDetails(getTicketDetails);
System.out.println("ABOUT TO MARSHALSENDANDRECEIVE...");
GetTicketDetailsResponse ticketDetailsResponse = (GetTicketDetailsResponse) this.template.marshalSendAndReceive(elGetTicketDetails);
return ticketDetailsResponse;
}
Interceptor:
#Override
public boolean handleRequest(MessageContext arg0) throws WebServiceClientException {
// TODO Auto-generated method stub
TransportContext context = TransportContextHolder.getTransportContext();
HttpComponentsConnection connection =(HttpComponentsConnection) context.getConnection();
try {
connection.addRequestHeader("username", "testu");
connection.addRequestHeader("password", "test");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
HttpLoggingUtils.logMessage("Client Request Message", arg0.getRequest());
return true;
}
Result Snippet (This is where I expected to see the username/password headers. Since they are missing, I'm guessing this is the issue):
ABOUT TO MARSHALSENDANDRECEIVE...
2021-08-09 13:46:18.891 INFO 23112 --- [ main] com.fp.fpcustomization.HttpLoggingUtils :
----------------------------
Client Request Message
----------------------------
Accept: text/xml, text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
SOAPAction: ""
Content-Type: text/xml; charset=utf-8
Content-Length: 378
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/><SOAP-ENV:Body><ns3:getTicketDetails xmlns:ns3="http://externalapi.business.footprints.numarasoftware.com/"><getItemDetailsRequest><_itemDefinitionId>76894</_itemDefinitionId><_itemId>30201</_itemId></getItemDetailsRequest></ns3:getTicketDetails></SOAP-ENV:Body></SOAP-ENV:Envelope>
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 2.151 s <<< FAILURE! - in com.fp.fpcustomization.FpcustomizationApplicationTests
[ERROR] RequestTest Time elapsed: 0.51 s <<< ERROR!
org.springframework.ws.client.WebServiceTransportException: [401]
at com.fp.fpcustomization.FpcustomizationApplicationTests.RequestTest(FpcustomizationApplicationTests.java:29)
Following up with the solution that worked for me. This time around, I looked at going about this via the callback function. This led me to the question asked here:
Add SoapHeader to org.springframework.ws.WebServiceMessage
The second answer by Pranav Kumar is what worked for me. So I simply added the callback function to the 'marshalSendAndReceive' function call in the 'GetTicketDetailsResponse getTicketDetails(long definitionId, long itemId)' method. This way, I was able to add the Authorization header that got me past the 401 error that I was getting before.
Object theResponseObject = this.template.marshalSendAndReceive((elGetTicketDetails), new WebServiceMessageCallback(){
#Override
public void doWithMessage(WebServiceMessage message) throws IOException, TransformerException {
SaajSoapMessage soapMessage = (SaajSoapMessage) message;
MimeHeaders mimeHeader = soapMessage.getSaajMessage().getMimeHeaders();
mimeHeader.setHeader("Authorization", "Basic ...==");
}
});
Following this addition, the Authorization header was showing up in the request and the response was successfully returned.

Request with multipart/form-data returns 415 error

I need to receive this request using Spring:
POST /test HTTP/1.1
user-agent: Dart/2.8 (dart:io)
content-type: multipart/form-data; boundary=--dio-boundary-3791459749
accept-encoding: gzip
content-length: 151
host: 192.168.0.107:8443
----dio-boundary-3791459749
content-disposition: form-data; name="MyModel"
{"testString":"hello world"}
----dio-boundary-3791459749--
But unfortunately this Spring endpoint:
#PostMapping(value = "/test", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void test(#Valid #RequestPart(value = "MyModel") MyModel myModel) {
String testString = myModel.getTestString();
}
returns 415 error:
Content type 'multipart/form-data;boundary=--dio-boundary-2534440849' not supported
to the client.
And this(same endpoint but with the consumes = MULTIPART_FORM_DATA_VALUE):
#PostMapping(value = "/test", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void test(#Valid #RequestPart(value = "MyModel") MyModel myModel) {
String testString = myModel.getTestString();
}
again returns 415 but, with this message:
Content type 'application/octet-stream' not supported
I already successfully used this endpoint(even without consumes) with this old request:
POST /test HTTP/1.1
Content-Type: multipart/form-data; boundary=62b81b81-05b1-4287-971b-c32ffa990559
Content-Length: 275
Host: 192.168.0.107:8443
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.8.0
--62b81b81-05b1-4287-971b-c32ffa990559
Content-Disposition: form-data; name="MyModel"
Content-Transfer-Encoding: binary
Content-Type: application/json; charset=UTF-8
Content-Length: 35
{"testString":"hello world"}
--62b81b81-05b1-4287-971b-c32ffa990559--
But unfortunately now I need to use the first described request and I can't add additional fields to it.
So, I need to change the Spring endpoint, but how?
You need to have your controller method consume MediaType.MULTIPART_FORM_DATA_VALUE,
#PostMapping(value = "/test", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
......
You also need to add a MappingJackson2HttpMessageConverter support application/octet-stream. In this answer,
I configure it by using WebMvcConfigurer#extendMessageConverters so that I can keep the default configuration of the other converters.(Spring MVC is configured with Spring Boot’s converters).
I create the converter from the ObjectMapper instance used by Spring.
[For more information]
Spring Boot Reference Documentation - Spring MVC Auto-configuration
How do I obtain the Jackson ObjectMapper in use by Spring 4.1?
Why does Spring Boot change the format of a JSON response even when a custom converter which never handles JSON is configured?
#Configuration
public class MyConfigurer implements WebMvcConfigurer {
#Autowired
private ObjectMapper objectMapper;
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
ReadOnlyMultipartFormDataEndpointConverter converter = new ReadOnlyMultipartFormDataEndpointConverter(
objectMapper);
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.addAll(converter.getSupportedMediaTypes());
supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
converter.setSupportedMediaTypes(supportedMediaTypes);
converters.add(converter);
}
}
[NOTE]
Also you can modify the behavior of your converter by extending it.
In this answer, I extends MappingJackson2HttpMessageConverter so that
it reads data only when the mapped controller method consumes just MediaType.MULTIPART_FORM_DATA_VALUE
it doesn't write any response(another converter do that).
public class ReadOnlyMultipartFormDataEndpointConverter extends MappingJackson2HttpMessageConverter {
public ReadOnlyMultipartFormDataEndpointConverter(ObjectMapper objectMapper) {
super(objectMapper);
}
#Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
// When a rest client(e.g. RestTemplate#getForObject) reads a request, 'RequestAttributes' can be null.
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return false;
}
HandlerMethod handlerMethod = (HandlerMethod) requestAttributes
.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
if (handlerMethod == null) {
return false;
}
RequestMapping requestMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
if (requestMapping == null) {
return false;
}
// This converter reads data only when the mapped controller method consumes just 'MediaType.MULTIPART_FORM_DATA_VALUE'.
if (requestMapping.consumes().length != 1
|| !MediaType.MULTIPART_FORM_DATA_VALUE.equals(requestMapping.consumes()[0])) {
return false;
}
return super.canRead(type, contextClass, mediaType);
}
// If you want to decide whether this converter can reads data depending on end point classes (i.e. classes with '#RestController'/'#Controller'),
// you have to compare 'contextClass' to the type(s) of your end point class(es).
// Use this 'canRead' method instead.
// #Override
// public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
// return YourEndpointController.class == contextClass && super.canRead(type, contextClass, mediaType);
// }
#Override
protected boolean canWrite(MediaType mediaType) {
// This converter is only be used for requests.
return false;
}
}
The causes of 415 errors
When your controller method consumes MediaType.APPLICATION_OCTET_STREAM_VALUE, it doesn't handle a request with Content-Type: multipart/form-data;. Therefore you get 415.
On the other hand, when your controller method consumes MediaType.MULTIPART_FORM_DATA_VALUE, it can handle a request with Content-Type: multipart/form-data;. However JSON without Content-Type is not handled depending on your configuration.
When you annotate a method argument with #RequestPart annotation,
RequestPartMethodArgumentResolver parses a request.
RequestPartMethodArgumentResolver recognizes content-type as application/octet-stream when it is not specified.
RequestPartMethodArgumentResolver uses a MappingJackson2HttpMessageConverter to parse a reuqest body and get JSON.
By default configuration MappingJackson2HttpMessageConverter supports application/json and application/*+json only.
(As far as I read your question) Your MappingJackson2HttpMessageConverters don't seem to support application/octet-stream.(Therefore you get 415.)
Conclusion
Therefore I think you can successfully handle a request by letting MappingJackson2HttpMessageConverter(an implementation of HttpMessageConverter) to support application/octet-stream like above.
[UPDATE 1]
If you don't need to validate MyModel with #Valid annotation and simply want to convert the JSON body to MyModel, #RequestParam can be useful.
If you choose this solution, you do NOT have to configure MappingJackson2HttpMessageConverter to support application/octet-stream.
You can handle not only JSON data but also file data using this solution.
#PostMapping(value = "/test", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void test(#RequestParam(value = "MyModel") Part part) throws IOException {
// 'part' is an instance of 'javax.servlet.http.Part'.
// According to javadoc of 'javax.servlet.http.Part',
// 'The part may represent either an uploaded file or form data'
try (InputStream is = part.getInputStream()) {
ObjectMapper objectMapper = new ObjectMapper();
MyModel myModel = objectMapper.readValue(part.getInputStream(), MyModel.class);
.....
}
.....
}
See Also
Javadoc of RequestPartMethodArgumentResolver
Javadoc of MappingJackson2HttpMessageConverter
Content type blank is not supported (Related question)
Spring Web MVC - Multipart

Spring Boot: getting query string parameters and request body in AuditApplicationEvent listener

Spring Boot REST app here. I'm trying to configure Spring Boot request auditing to log each and every HTTP request that any resources/controllers receive with the following info:
I need to see in the logs the exact HTTP URL (path) that was requested by the client, including the HTTP method and any query string parameters; and
If there is a request body (such as with a POST or PUT) I need to see the contents of that body in the logs as well
My best attempt so far:
#Component
public class MyAppAuditor {
private Logger logger;
#EventListener
public void handleAuditEvent(AuditApplicationEvent auditApplicationEvent) {
logger.info(auditApplicationEvent.auditEvent);
}
}
public class AuditingTraceRepository implements TraceRepository {
#Autowired
private ApplicationEventPublisher applicationEventPublisher
#Override
List<Trace> findAll() {
throw new UnsupportedOperationException("We don't expose trace information via /trace!");
}
#Override
void add(Map<String, Object> traceInfo) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
AuditEvent traceRequestEvent = new AuditEvent(new Date(), "SomeUser", 'http.request.trace', traceInfo);
AuditApplicationEvent traceRequestAppEvent = new AuditApplicationEvent(traceRequestEvent);
applicationEventPublisher.publishEvent(traceRequestAppEvent);
}
}
However at runtime if I use the following curl command:
curl -i -H "Content-Type: application/json" -X GET 'http://localhost:9200/v1/data/profiles?continent=NA&country=US&isMale=0&height=1.5&dob=range('1980-01-01','1990-01-01')'
Then I only see the following log messages (where MyAppAuditor send audit events):
{ "timestamp" : "14:09:50.516", "thread" : "qtp1293252487-17", "level" : "INFO", "logger" : "com.myapp.ws.shared.auditing.MyAppAuditor", "message" : {"timestamp":"2018-06-29T18:09:50+0000","principal":"SomeUser","type":"http.request.trace","data":{"method":"GET","path":"/v1/data/profiles","headers":{"request":{"User-Agent":"curl/7.54.0","Host":"localhost:9200","Accept":"*/*","Content-Type":"application/json"},"response":{"X-Frame-Options":"DENY","Cache-Control":"no-cache, no-store, max-age=0, must-revalidate","X-Content-Type-Options":"nosniff","Pragma":"no-cache","Expires":"0","X-XSS-Protection":"1; mode=block","X-Application-Context":"application:9200","Date":"Fri, 29 Jun 2018 18:09:50 GMT","Content-Type":"application/json;charset=utf-8","status":"200"}},"timeTaken":"89"}} }
So as you can see, the auditor is picking up the base path (/v1/data/profiles) but is not logging any of the query string parameters. I also see a similar absence of request body info when I hit POST or PUT endpoints that do require a request body (JSON).
What do I need to do to configure these classes (or other Spring classes/configs) so that I get the level of request auditing that I'm looking for?
Fortunately, Actuator makes it very easy to configure those Trace events.
Adding parameters to Trace info
You can take a look at all of the options. You'll notice the defaults (line 42) are:
Include.REQUEST_HEADERS,
Include.RESPONSE_HEADERS,
Include.COOKIES,
Include.ERRORS,
Include.TIME_TAKEN
So you'll need to also add Include.PARAMETERS and anything else you'd like to have in the trace. To configure that, there's a configuration property for that management.trace.include.
So to get what you want (i.e. parameters), plus the defaults, you'd have:
management.trace.include = parameters, request-headers, response-headers, cookies, errors, time-taken
Adding request body to Trace info
In order to get the body, you're going to have to add in this Bean to your Context:
#Component
public class WebRequestTraceFilterWithPayload extends WebRequestTraceFilter {
public WebRequestTraceFilterWithPayload(TraceRepository repository, TraceProperties properties) {
super(repository, properties);
}
#Override
protected Map<String, Object> getTrace(HttpServletRequest request) {
Map<String, Object> trace = super.getTrace(request);
String body = null;
try {
body = request.getReader().lines().collect(Collectors.joining("\n"));
} catch (IOException e) {
throw new RuntimeException(e);
}
if(body != null) {
trace.put("body", body);
}
return trace;
}
}
The above code will override the AutoConfigure'd WebRequestTraceFilter bean (which, because it is #ConditionalOnMissingBean will give deference to your custom bean), and pull the extra payload property off of the request then add it to to the Map properties that get published to your TraceRepository!
Summary
Request Parameters can be added to TraceRepository trace events by via the management.trace.include property
The Request Body can be added to the TraceRepository trace events by creating an extended Bean to read the body off of the HTTP request and supplementing the trace events

In spring, how about if PathVariable contains RequestMapping value

If I want to create a basic controller with RequestMapping = "/{content}" to handle the general case. But for some specific contents, I want to create a concrete controller for this special case, and inherit from that basic controller.
For example:
#RequestMapping(value = "/{content}")
class ContentController {
public ContentController(#PathVariable String content) { ... }
}
#RequestMapping(value = "/specialContent")
class SpecialContentController extends ContentController {
public SpecialContentController() { super("specialContent"); }
// overwrite sth
....
}
Is this legal? Or some other better implementation?
#PathVariable should not be used in constructor.
You seem confused about how controllers in spring work.
A controller is a singleton which is created on application upstart and whose methods are invoked to handle incoming requests.
Because your controller isn't created for each request but created before any requests are handled you can't use path variables in the constructor - both because there's no information about it's value when the instance is created but also because you'll want it to reflect the current request being handled and since controllers can handle many multiple requests simultaneously you can't store it as a class attribute or multiple requests would interfere with each other.
To achieve what you want you should use methods and compose them, something like this:
#RestController
public class ContentController {
#GetMapping("/specialContent")
public Map<String, String> handleSpecialContent() {
Map<String, String> map = handleContent("specialContent");
map.put("special", "true");
return map;
}
#GetMapping("/{content}")
public Map<String, String> handleContent(#PathVariable String content) {
HashMap<String, String> map = new HashMap<>();
map.put("content", content);
return map;
}
}
Note the regular expression in {content:^(?!specialContent$).*$} to ensure that Spring never routes specialContent there. You can get an explanation of the regular expression here and toy around with it here.
You can see that it works if we put it to the test:
$ http localhost:8080/test
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Thu, 01 Feb 2018 08:18:11 GMT
Transfer-Encoding: chunked
{
"content": "test"
}
$ http localhost:8080/specialContent
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Thu, 01 Feb 2018 08:18:15 GMT
Transfer-Encoding: chunked
{
"content": "specialContent",
"special": "true"
}

apache Shiro Login

I am kind of new to Apache shiro and trying to use authcBasic for securing the webservice.
I need to create a webservice using which I can login by providing username and password which can utilize apache shiro's features.
Any guidance will highly be appreciated
I have created a minimal example application with Spring-Boot (because of the "spring" tag) and Shiro for you, which you can find here on GitHub. The example application is based on the "hello world" RESTful web service with Spring application from the Spring docs. I have added Shiro to it via these changes (GitHub commit):
Add the shiro-spring dependency to pom.xml:
</dependencies>
[...]
<!-- Apache Shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
Copy shiro.ini from the Shiro docs to resources:
# =============================================================================
# Tutorial INI configuration
#
# Usernames/passwords are based on the classic Mel Brooks' film "Spaceballs" :)
# =============================================================================
# -----------------------------------------------------------------------------
# Users and their (optional) assigned roles
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz
# -----------------------------------------------------------------------------
# Roles with assigned permissions
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
Configure ShiroFilter, SecurityManager with IniRealm, and Shiro annotations in Application.java (adapted from here):
#SpringBootApplication
public class Application {
[...]
#Bean(name = "shiroFilter")
public FilterRegistrationBean shiroFilter() throws Exception {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter((AbstractShiroFilter) getShiroFilterFactoryBean().getObject());
registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
return registration;
}
#Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
filterChainDefinitionMap.put("/**", "authcBasic");
return shiroFilterFactoryBean;
}
#Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();
dwsm.setRealm(getShiroIniRealm());
final DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// disable session cookie
sessionManager.setSessionIdCookieEnabled(false);
dwsm.setSessionManager(sessionManager);
return dwsm;
}
#Bean(name = "shiroIniRealm")
#DependsOn("lifecycleBeanPostProcessor")
public IniRealm getShiroIniRealm() {
return new IniRealm("classpath:shiro.ini");
}
#Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
#Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
daap.setProxyTargetClass(true);
return daap;
}
#Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
aasa.setSecurityManager(securityManager());
return new AuthorizationAttributeSourceAdvisor();
}
}
Add #RequiresRoles annotation with parameter "admin" to GreetingController for testing purposes:
#RestController
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
#RequestMapping("/greeting")
#RequiresRoles(value = {"admin"})
public Greeting greeting(#RequestParam(value="name", defaultValue="World") String name) {
return new Greeting(counter.incrementAndGet(),
String.format(template, name));
}
}
Use the following commands to check out and run the application:
git clone https://github.com/opncow/gs-rest-service.git
cd gs-rest-service/complete/
./mvnw spring-boot:run
Verify that Shiro is working (use HttpRequester or similar plugin to create the following requests):
User "root" (has "admin" role) with password "secret" (Base64 encoded username:password as value of the Authorization header)
GET http://localhost:8080/greeting
Authorization: Basic cm9vdDpzZWNyZXQ=
-- response --
200
Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Thu, 11-May-2017 00:29:44 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 12 May 2017 00:29:44 GMT
{"id":1,"content":"Hello, World!"}
User "guest" with password "guest" (no "admin" role):
GET http://localhost:8080/greeting
Authorization: Basic Z3Vlc3Q6Z3Vlc3Q=
-- response --
500
Set-Cookie: rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Thu, 11-May-2017 00:44:18 GMT rememberMe=deleteMe; Path=/; Max-Age=0; Expires=Thu, 11-May-2017 00:44:18 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 12 May 2017 00:44:18 GMT
Connection: close
{"timestamp":1494549858572,"status":500,"error":"Internal Server Error","exception":"org.apache.shiro.authz.UnauthorizedException","message":"Subject does not have role [admin]","path":"/greeting"}
As can be seen, in the second request, the user guest is authenticated, however not authorized to use the greeting resource because of lacking the "admin" role (which means that the annotation is working).
This is the most minimal example I could imagine. It uses Shiro's .ini configuration/realm for users, passwords, and roles. For a real world project you will likely have to use a more sophisticated realm implementation such as Shiro's JdbcRealm

Resources