Table of contents
Open Table of contents
Article body
The integration of Shiro, Spring Boot, and JWT is now complete. The full code follows.
First: shiroConfig, the overall Shiro configuration class.
Here, I leave session management to Shiro’s defaults instead of adding extensive configuration. The default session duration is sufficient, and there is no need to make the configuration class so large. An earlier Shiro article covered sessions specifically, including integration with Quartz. See that article.
/**
* Shiro configuration class
*/
@Configuration
public class ShiroConfig {
@Bean("rememberCookie")
public SimpleCookie rememberCookie(){
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setHttpOnly(true);
simpleCookie.setName("remeberCookie");
simpleCookie.setMaxAge(360000);
return simpleCookie;
}
@Bean("cookieRememberMe")
public CookieRememberMeManager cookieRememberMe(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberCookie());
return cookieRememberMeManager;
}
/**
* Password hashing
* @return
*/
@Bean("credentialsMatcher")
public HashedCredentialsMatcher credentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("SHA-256");
hashedCredentialsMatcher.setHashIterations(20);
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
@Bean("shiroRealm")
public ShiroRealm shiroRealm(){
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCredentialsMatcher(credentialsMatcher());
return shiroRealm;
}
/**
* Leave session management to Shiro’s defaults without detailed configuration.
* @return
*/
@Bean("securityManager")
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//Pass in the custom shiroRealm.
securityManager.setRealm(shiroRealm());
//Note that setRememberMeManager takes a CookieRememberMeManager; use the correct type.
securityManager.setRememberMeManager(cookieRememberMe());
return securityManager;
}
/**
* Do not set loginUrl here. It can be configured in a traditional application without a separate frontend.
* With a separate frontend, the frontend routes to the login page when no token is available. There is no need to locate a view through loginUrl; Spring Boot handles only the backend.
* Also note that intercepted paths in filterMap must start with /, otherwise the corresponding Controller cannot be found.
* Remember: put /** = authc last!!!!!
* Update on 2019-05-12: loginUrl is ultimately still required because the filter checks whether the request URL is the login URL.
* @param securityManager
* @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,AuthFilter authFilter){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/user/login");
//Set the custom filter.
Map<String, Filter> filter = new HashMap<>();
filter.put("auth",authFilter);
shiroFilterFactoryBean.setFilters(filter);
//Use LinkedHashMap to preserve the order of the interceptors.
Map<String,String> filterMap = new LinkedHashMap<>();
//Do not use anno for login here; route everything through the custom filter.
//filterMap.put("/user/login","anon");
filterMap.put("/user/insert","auth");
filterMap.put("/**","auth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
/**
* Let Spring manage the Shiro lifecycle.
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
}
Second: shiroRealm, which handles Shiro authentication and authorization.
/**
* Custom shiroRealm
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
UserService service;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
User user = YOUR_DATABASE_QUERY_IMPLEMENTATION;
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user,user.getPassword(), ByteSource.Util.bytes(user.getSalt()),getName());
return simpleAuthenticationInfo;
}
}
Third: AuthFilter, the filter through which all Shiro requests must pass.
/**
* Shiro filter
*/
@Component("authFilter")
public class AuthFilter extends FormAuthenticationFilter {
@Autowired
JwtUtil jwtUtil;
/**
* Check whether the token is empty or expired.
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
String token = getRequestToken((HttpServletRequest) request);
if (ObjectUtils.isNull(token)){
return false;
}
if (StringUtils.isBlank(token)) {
throw new CustomException(jwtUtil.getHeader()+"不能为空", HttpStatus.SC_UNAUTHORIZED);
}
Claims claims = jwtUtil.parseToken(token);
if (ObjectUtils.isNull(claims) || jwtUtil.isTokenExpired(claims.getExpiration())) {
throw new CustomException(jwtUtil.getHeader()+"token过期",HttpStatus.SC_UNAUTHORIZED);
}
return true;
}
/**
* This method runs next if the method above returns false; it does not run if that method returns true.
* Check whether this is the login URL, then whether the request is a POST.
*
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
return true;
}
}
return false;
}
/**
* Get the token from the request header first; if absent, try the request parameter.
*
* @param request
* @return
*/
private String getRequestToken(HttpServletRequest request) {
String token = request.getHeader(jwtUtil.getHeader());
if (StringUtils.isBlank(token)) {
token = request.getParameter(jwtUtil.getHeader());
}
return token;
}
}
Fourth: JwtUtil, which creates and validates JWTs.
/**
* JWT utility for token generation and validation.
*/
@ConfigurationProperties(prefix = "dhb.jwt")
@Component
public class JwtUtil {
private Logger logger = LoggerFactory.getLogger(getClass());
private String secret;
private Long expire;
private String header;
/**
*
* Generate a JWT token.
* @param userId
* @return
*/
public String generateToken(Long userId) {
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(userId + "")
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
/**
*
* Parse a JWT token.
* @param token
* @return
*/
public Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
logger.info("Error parsing token");
return null;
}
}
/**
*
* Check whether the token has expired.
* @param expiprationTime
* @return
*/
public boolean isTokenExpired(Date expiprationTime){
return expiprationTime.before(new Date());
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public Long getExpire() {
return expire;
}
public void setExpire(Long expire) {
this.expire = expire;
}
public String getHeader() {
return header;
}
public void setHeader(String header) {
this.header = header;
}
}
A final thought: although this looks like just four major steps, looking back, it took me a long time to get here. I started by learning Shiro’s architecture and basic usage, then worked through pitfalls in practice, considered why to use tokens instead of sessions, and decided to drop the complicated session configuration and Quartz integration. At every step I asked myself why I needed a particular technology and how to keep the final integration from becoming bloated.
For everyday API testing, I recommend Postman or YApi. Avoid building a whole page just to test one small endpoint: it distracts you from the main work, and you may end up doing none of it well.
This series does not provide a complete standalone codebase, but one of my projects with separate frontend and backend components integrates Shiro. If you need it, the code is here.
As I keep saying: if you are going to do it, do it carefully enough to be proud of your work!