Spring Boot Shiro
Integrate Apache Shiro into spring boot 2.
Add dependency
Add shiro-spring-boot-web-starter
to maven pom.
Shiro in yaml
shiro:
loginUrl: /login
successUrl: /secure
unauthorizedUrl: /login
sessionManager:
sessionIdUrlRewritingEnabled: false
Implements Custom Realm
public class UserRealm extends AuthorizingRealm {
@Autowired
private CustomerService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Subject currentUser = SecurityUtils.getSubject();
User user = (User) currentUser.getSession().getAttribute("user");
if (user != null) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRoles(user.getRoleList());
return authorizationInfo;
}
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
User user = new User();
//set user info from token
User dbUser = this.userService.getUser(user);
if (dbUser != null) {
Subject currentUser = SecurityUtils.getSubject();
currentUser.getSession().setAttribute("user", dbUser);
return new SimpleAuthenticationInfo(upToken.getUsername(), dbUser.getPwd().toCharArray(), getName());
}
return null;
}
}
Shiro Config
@Configuration
public class ShiroConfig {
@ExceptionHandler(AuthorizationException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public String handleException(AuthorizationException e, Model model) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", HttpStatus.FORBIDDEN.value());
map.put("message", "No message available");
model.addAttribute("errors", map);
return "error";
}
@Bean
public UserRealm myShiroRealm() {
UserRealm myShiroRealm = new UserRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
return hashedCredentialsMatcher;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/", "anon");
chainDefinition.addPathDefinition("/login", "anon");
chainDefinition.addPathDefinition("/logout", "logout");
chainDefinition.addPathDefinition("/**/*", "authc");
return chainDefinition;
}
}
Login
Add login logic in your controller.
@Controller
public class UserController {
@GetMapping("/")
public String index() {
return "index";
}
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(HttpServletRequest req, UserCredentials cred, RedirectAttributes attr) {
Subject subject = SecurityUtils.getSubject();
if (!subject.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken(cred.getUsername(), cred.getPassword(),
cred.isRememberMe());
try {
subject.login(token);//
} catch (AuthenticationException ae) {
attr.addFlashAttribute("error", "Invalid Credentials");
return "redirect:/login";
}
}
return "redirect:/secure";
}
}
Shiro ajax
When session timeout, if a secure resource is called by ajax, there is no any hints to user or page cannot be redirected to login page by default.
In order to handle this case, developer needs to override onAccessDenied
of FormAuthenticationFilter
to return specified response status when session timeout:
public class CustomShiroAuthcFilter extends FormAuthenticationFilter{
public CustomShiroAuthcFilter() {
super();
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
return super.onAccessDenied(request, response);
} else {
if (isAjax((HttpServletRequest) request)) {
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
} else {
saveRequestAndRedirectToLogin(request, response);
}
return false;
}
}
private boolean isAjax(HttpServletRequest request) {
String requestedWithHeader = request.getHeader("X-Requested-With");
return "XMLHttpRequest".equals(requestedWithHeader);
}
}
and add ShiroFilterFactoryBean
manually:
@Configuration
public class ShiroConfig {
@ExceptionHandler(AuthorizationException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public String handleException(AuthorizationException e, Model model) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", HttpStatus.FORBIDDEN.value());
map.put("message", "No message available");
model.addAttribute("errors", map);
return "error";
}
@Bean("authorizer")
public UserRealm myShiroRealm() {
UserRealm myShiroRealm = new UserRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
return hashedCredentialsMatcher;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SessionsSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/");
shiroFilterFactoryBean.setSuccessUrl("/main");
shiroFilterFactoryBean.setUnauthorizedUrl("/");
Map<String, Filter> filtersMap = new LinkedHashMap<>();
filtersMap.put("authc", new CustomShiroAuthcFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/css/*", "anon");
filterChainDefinitionMap.put("/image/*", "anon");
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/index", "anon");
filterChainDefinitionMap.put("/**/*", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
}
Written on December 11, 2018