I need to extend spring security to hash the http response content and place the result inside of a header. My approach is to create a servlet filter that reads the response and places the appropriate header. The filter is registered with spring security via a separate plugin. The implementation is largely taken from here.
The entire setup works perfectly when the final application uses "render" in the controller to output JSON to the client. However, if the same data is formatted via "respond" a 404 is returned to the client. I am at a loss to explain the difference.
For reference everything is grails version 2.3.11 and spring security core version 2.0-RC4
Register the filter via my plugin's doWithSpring
responseHasher(ResponseHasher)
SpringSecurityUtils.registerFilter(
'responseHasher', SecurityFilterPosition.LAST.order - 1)
My filter implementation
public class ResponseHasher implements Filter{
@Override
public void init(FilterConfig config) throws ServletException {
}
@Override
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletResponseCopier wrapper = new HttpServletResponseCopier((HttpServletResponse)response);
chain.doFilter(request, wrapper);
wrapper.flushBuffer();
/*
Take the response, hash it, and set it in a header. for brevity sake just prove we can read it for now
and set a static header
*/
byte[] copy = wrapper.getCopy();
System.out.println(new String(copy, response.getCharacterEncoding()));
wrapper.setHeader("foo","bar");
}
@Override
public void destroy() {
}
}
The HttpServletResponseCopier implementation. The only change from the source is to override all 3 method signatures of write instead of just the one.
class HttpServletResponseCopier extends HttpServletResponseWrapper{
private ServletOutputStream outputStream;
private PrintWriter writer;
private ServletOutputStreamCopier copier;
public HttpServletResponseCopier(HttpServletResponse response) throws IOException {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (writer != null) {
throw new IllegalStateException("getWriter() has already been called on this response.");
}
if (outputStream == null) {
outputStream = getResponse().getOutputStream();
copier = new ServletOutputStreamCopier(outputStream);
}
return copier;
}
@Override
public PrintWriter getWriter() throws IOException {
if (outputStream != null) {
throw new IllegalStateException("getOutputStream() has already been called on this response.");
}
if (writer == null) {
copier = new ServletOutputStreamCopier(getResponse().getOutputStream());
writer = new PrintWriter(new OutputStreamWriter(copier, getResponse().getCharacterEncoding()), true);
}
return writer;
}
@Override
public void flushBuffer() throws IOException {
if (writer != null) {
writer.flush();
} else if (outputStream != null) {
copier.flush();
}
}
public byte[] getCopy() {
if (copier != null) {
return copier.getCopy();
} else {
return new byte[0];
}
}
private class ServletOutputStreamCopier extends ServletOutputStream {
private OutputStream outputStream;
private ByteArrayOutputStream copy;
public ServletOutputStreamCopier(OutputStream outputStream) {
this.outputStream = outputStream;
this.copy = new ByteArrayOutputStream(1024);
}
@Override
public void write(int b) throws IOException {
outputStream.write(b);
copy.write(b);
}
@Override
public void write(byte[] b,int off, int len) throws IOException {
outputStream.write(b,off,len);
copy.write(b,off,len);
}
@Override
public void write(byte[] b) throws IOException {
outputStream.write(b);
copy.write(b);
}
public byte[] getCopy() {
return copy.toByteArray();
}
}
}
And finally my controller method in the actual application
@Secured()
def myAction() {
def thing = Thing.get(1) //thing can be any domain object really. in this case we created thing 1 in bootstap
//throws a 404
respond(thing)
/*
works as expected, output is both rendered
and sent to system out, header "foo" is in response
/*
//render thing as JSON
}
Any insight would be appreciated as I do not understand why render would work and respond would not. Additionally, I'm open to other approaches to solving this need if what I am attempting just will not work in grails. Thanks in advance.
OOTB Spring security provides several securities filters. It is typically not necessary to know every filter but keep in mind that they work in certain order or sequence. Let’s look at some important filters.
Grails spring-security-core plugin Owner:grails| 4.0.3| Oct 16, 2020 | Package| Issues| Source| Documentation| License:Apache-2.0 241 dependencies { compile 'org.grails.plugins:spring-security-core:4.0.3' }
GSP features the ability to automatically HTML encode GSP expressions, and as of Grails 2.3 this is the default configuration. The default configuration (found in application.yml) for a newly created Grails application can be seen below: GSP features several codecs that it uses when writing the page to the response.
Grails has no default mechanism for authentication as it is possible to implement authentication in many different ways. It is however, easy to implement a simple authentication mechanism using interceptors.
I put all the project in grails, and got a similar problem. I had to make some changes.
For register, I used SpringSecurityUtils.clientRegisterFilter
method, as I was doing from a Bootstrap.groovy
application.
Also, I declared the filter in resources.groovy
It worked with render, but 404 with respond. So changed the respond to:
respond user, [formats:['json']]
And it worked after I removed your filter. I got 404 whenever I put your filter and it was trying to find action.gsp.
I made a change in ServletOutputStreamCopier
, implementing the delegates for close
and flush
methods, and it worked fine for render and respond:
class HttpServletResponseCopier extends HttpServletResponseWrapper {
private ServletOutputStream outputStream;
private PrintWriter writer;
private ServletOutputStreamCopier copier;
public HttpServletResponseCopier(HttpServletResponse response)
throws IOException {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (writer != null) {
throw new IllegalStateException(
"getWriter() has already been called on this response.");
}
if (outputStream == null) {
outputStream = getResponse().getOutputStream();
copier = new ServletOutputStreamCopier(outputStream);
}
return copier;
}
@Override
public PrintWriter getWriter() throws IOException {
if (outputStream != null) {
throw new IllegalStateException(
"getOutputStream() has already been called on this response.");
}
if (writer == null) {
copier = new ServletOutputStreamCopier(getResponse()
.getOutputStream());
writer = new PrintWriter(new OutputStreamWriter(copier,
getResponse().getCharacterEncoding()), true);
}
return writer;
}
@Override
public void flushBuffer() throws IOException {
if (writer != null) {
writer.flush();
} else if (outputStream != null) {
copier.flush();
}
}
public byte[] getCopy() {
if (copier != null) {
return copier.getCopy();
} else {
return new byte[0];
}
}
private class ServletOutputStreamCopier extends ServletOutputStream {
private OutputStream outputStream;
private ByteArrayOutputStream copy;
public ServletOutputStreamCopier(OutputStream outputStream) {
this.outputStream = outputStream;
this.copy = new ByteArrayOutputStream(1024);
}
@Override
public void write(int b) throws IOException {
outputStream.write(b);
copy.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
outputStream.write(b, off, len);
copy.write(b, off, len);
}
@Override
public void write(byte[] b) throws IOException {
outputStream.write(b);
copy.write(b);
}
@Override
public void flush() throws IOException {
outputStream.flush();
copy.flush();
}
@Override
public void close() throws IOException {
outputStream.close();
copy.close();
}
public byte[] getCopy() {
return copy.toByteArray();
}
}
}
I didn't go through respond implementation details, but I think it get some confusion as it don't have a way to flush or close, and has a fallback to call the view instead of render json.
I know it is a little bit late, but now it is working.
resources.groovy
beans = {
responseHasher(ResponseHasher){
}
}
Boostrap.groovy
def init = { servletContext ->
.
SpringSecurityUtils.clientRegisterFilter('responseHasher', SecurityFilterPosition.LAST.order - 1)
}
Best, Eder
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With