1. First, we will add one annotation to enable global method security (highlighted below) to the security config file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | package com.dtr.oas.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ComponentScan; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; import org.springframework.security.core.userdetails.UserDetailsService; import com.dtr.oas.controller.AccessDeniedExceptionHandler; @Configuration @EnableWebMvcSecurity @ComponentScan (basePackageClasses=com.dtr.oas.service.UserServiceImpl. class ) @EnableGlobalMethodSecurity (prePostEnabled= true ) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired public void configureGlobal(UserDetailsService userDetailsService, AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(userDetailsService); } @Autowired AccessDeniedExceptionHandler accessDeniedExceptionHandler; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers( "/resources/**" ).permitAll() .antMatchers( "/error/**" ).permitAll() .antMatchers( "/strategy/**" ).hasRole( "ADMIN" ) .anyRequest().authenticated() .and() .formLogin() .loginPage( "/login" ) .defaultSuccessUrl( "/" ) .permitAll() .and() .logout() .permitAll() .and() .exceptionHandling() .accessDeniedHandler(accessDeniedExceptionHandler); } } |
2. Second, we will add a pre-authorize annotation (highlighted below) to to the strategy controller.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | package com.dtr.oas.controller; import java.util.List; import java.util.Locale; import javax.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import com.dtr.oas.model.Strategy; import com.dtr.oas.service.StrategyService; @Controller @RequestMapping (value = "/strategy" ) @PreAuthorize ( "hasRole('ROLE_ADMIN')" ) public class StrategyController { static Logger logger = LoggerFactory.getLogger(StrategyController. class ); @Autowired private StrategyService strategyService; @Autowired private MessageSource messageSource; @RequestMapping (value = { "/" , "/list" }, method = RequestMethod.GET) public String listOfStrategies(Model model) { logger.info( "IN: Strategy/list-GET" ); List<Strategy> strategies = strategyService.getStrategies(); model.addAttribute( "strategies" , strategies); // if there was an error in /add, we do not want to overwrite // the existing strategy object containing the errors. if (!model.containsAttribute( "strategy" )) { logger.info( "Adding Strategy object to model" ); Strategy strategy = new Strategy(); model.addAttribute( "strategy" , strategy); } return "strategy-list" ; } @RequestMapping (value = "/add" , method = RequestMethod.POST) public String addingStrategy( @Valid @ModelAttribute Strategy strategy, BindingResult result, RedirectAttributes redirectAttrs) { logger.info( "IN: Strategy/add-POST" ); if (result.hasErrors()) { logger.info( "Strategy-add error: " + result.toString()); redirectAttrs.addFlashAttribute( "org.springframework.validation.BindingResult.strategy" , result); redirectAttrs.addFlashAttribute( "strategy" , strategy); return "redirect:/strategy/list" ; } else { strategyService.addStrategy(strategy); String message = "Strategy " + strategy.getId() + " was successfully added" ; redirectAttrs.addFlashAttribute( "message" , message); return "redirect:/strategy/list" ; } } @RequestMapping (value = "/edit" , method = RequestMethod.GET) public String editStrategyPage( @RequestParam (value = "id" , required = true ) Integer id, Model model) { logger.info( "IN: Strategy/edit-GET: ID to query = " + id); if (!model.containsAttribute( "strategy" )) { logger.info( "Adding Strategy object to model" ); Strategy strategy = strategyService.getStrategy(id); logger.info( "Strategy/edit-GET: " + strategy.toString()); model.addAttribute( "strategy" , strategy); } return "strategy-edit" ; } @RequestMapping (value = "/edit" , method = RequestMethod.POST) public String editingStrategy( @Valid @ModelAttribute Strategy strategy, BindingResult result, RedirectAttributes redirectAttrs, @RequestParam (value = "action" , required = true ) String action) { logger.info( "IN: Strategy/edit-POST: " + action); if (action.equals(messageSource.getMessage( "button.action.cancel" , null , Locale.US))) { String message = "Strategy " + strategy.getId() + " edit cancelled" ; redirectAttrs.addFlashAttribute( "message" , message); } else if (result.hasErrors()) { logger.info( "Strategy-edit error: " + result.toString()); redirectAttrs.addFlashAttribute( "org.springframework.validation.BindingResult.strategy" , result); redirectAttrs.addFlashAttribute( "strategy" , strategy); return "redirect:/strategy/edit?id=" + strategy.getId(); } else if (action.equals(messageSource.getMessage( "button.action.save" , null , Locale.US))) { logger.info( "Strategy/edit-POST: " + strategy.toString()); strategyService.updateStrategy(strategy); String message = "Strategy " + strategy.getId() + " was successfully edited" ; redirectAttrs.addFlashAttribute( "message" , message); } return "redirect:/strategy/list" ; } @RequestMapping (value = "/delete" , method = RequestMethod.GET) public String deleteStrategyPage( @RequestParam (value = "id" , required = true ) Integer id, @RequestParam (value = "phase" , required = true ) String phase, Model model) { Strategy strategy = strategyService.getStrategy(id); logger.info( "IN: Strategy/delete-GET | id = " + id + " | phase = " + phase + " | " + strategy.toString()); if (phase.equals(messageSource.getMessage( "button.action.cancel" , null , Locale.US))) { String message = "Strategy delete was cancelled." ; model.addAttribute( "message" , message); return "redirect:/strategy/list" ; } else if (phase.equals(messageSource.getMessage( "button.action.stage" , null , Locale.US))) { String message = "Strategy " + strategy.getId() + " queued for display." ; model.addAttribute( "strategy" , strategy); model.addAttribute( "message" , message); return "strategy-delete" ; } else if (phase.equals(messageSource.getMessage( "button.action.delete" , null , Locale.US))) { strategyService.deleteStrategy(id); String message = "Strategy " + strategy.getId() + " was successfully deleted" ; model.addAttribute( "message" , message); return "redirect:/strategy/list" ; } return "redirect:/strategy/list" ; } } |
3. To test that the method level security is active, we can comment out the line in our security configuration that only allows the ADMIN role to access the strategy pages. The line to comment out is:
.antMatchers("/strategy/**").hasRole("ADMIN")
With this line commented out, the only way we could receive an access denied exception is if our method level security intercepts our request for the strategy pages.
4. When we log in as the user "user", and attempt to access a strategy page, we should see our custom 403 error page. We are now receiving this message because of the method level security rather than the HttpSecurity check.
The approach outlined above works well, but what about the case where we need to add a new role, or the case where we want to expand access to functionality for an existing role. For these two cases, using the approach outlined above, we would need to modify our code to change method level authorization. Another approach would be to have unique authorization specified at the method level, with these unique method level permissions entered into a permissions table. We could then create a link table to loosely couple permissions to roles. In the next post, we will add this structure to the example application.
Code at GitHub: https://github.com/dtr-trading/spring-ex13-auth
No comments:
Post a Comment