技术栈:Spring boot + Shiro +JWT
先说一下 Spring security 和 Shiro ,从这两者选择的时候最后还是选择了Shiro,原因是Spring security 偏重,适合大型企业项目,而且现在用Shiro的也不少。网上这两个的对比文章还是很多的,这里就不赘述。
Shiro默认实现的是session形式,也就是有状态的。我们要改变一些东西,来实现无状态的RESTful 架构~
1. pom.xml
只需要加入这两个包就可以
org.apache.shiro
shiro-spring
1.4.0
com.auth0
java-jwt
3.8.0
2. JWT配置
1. JWTToken.java 实体类
public class JWTToken implements AuthenticationToken {
private String token;
public JWTToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
2. JWTUtil.java 来实现JWT的token解析等工具类
public class JWTUtil {
// 过期时间一小时
private static final long EXPIRE_TIME = 60*60*1000;
private static final Logger PLOG = LoggerFactory.getLogger(JWTUtil.class);
/**
* 校验token是否正确
* @param token TOKEN
* @param secret 用户的密码
* @return boolean
*/
public static boolean verify(String token, String username, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e) {
PLOG.error("JWT >> " + e);
return false;
}
}
/**
* 无需解密直接获得token中的用户名
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
PLOG.error("JWT >> " + e);
return null;
}
}
/**
* 生成签名,并设置过期时间
* @param username 用户名
* @param secret 用户的密码
* @return token
*/
public static String sign(String username, String secret) {
try {
Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
PLOG.error("JWT >> " + e);
return null;
}
}
}
3. JWTFilter.java 我们要加一个我们自己的Shiro过滤器,并配置在Shiro
public class JWTFilter extends BasicHttpAuthenticationFilter {
private static final Logger PLOG = LoggerFactory.getLogger(JWTFilter.class);
/**
* 判断是否带TOKEN请求
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader("Authorization");
return !StringUtils.isEmpty(authorization);
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String authorization = httpServletRequest.getHeader("Authorization");
JWTToken token = new JWTToken(authorization);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 这里控制通过与否
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// OPTIONS 预请求 忽略
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
// 如果不带TOKEN请求,直接阻止
if (!isLoginAttempt(request, response)) {
throw new AuthenticationException("token is empty");
}
try {
executeLogin(request, response);
} catch (Exception e) {
PLOG.error("JWT >> " + e);
responseError(request, response);
return false;
}
return true;
}
/**
* 将非法请求跳转到 /ign/error
*/
private void responseError(ServletRequest req, ServletResponse resp) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.sendRedirect("/user/ign/error");
} catch (IOException e) {
PLOG.error("JWT >> " + e);
}
}
}
3. Shiro配置
CustomRealm.java
doGetAuthenticationInfo中抛出异常来进行身份判定
public class CustomRealm extends AuthorizingRealm {
private UserMapper userMapper;
private static final Logger PLOG = LoggerFactory.getLogger(CustomRealm.class);
@Autowired
private void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 获取身份验证信息
* Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
*
* @param authenticationToken 用户身份信息 token
* @return 返回封装了用户信息的 AuthenticationInfo 实例
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
PLOG.info("Shiro >> 身份认证");
String token = (String) authenticationToken.getCredentials();
if (token == null) {
throw new AuthenticationException("token invalid");
}
String username = JWTUtil.getUsername(token);
if (username == null) {
throw new AuthenticationException("token invalid");
}
// 从数据库获取对应用户名密码的用户
String password = userMapper.getPasswordByUsername(username);
if (null == password) {
throw new AuthenticationException("User didn't existed!");
}
if (!JWTUtil.verify(token, username, password)) {
throw new AuthenticationException("Username or password error");
}
return new SimpleAuthenticationInfo(token, token, getName());
}
/**
* 获取授权信息
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
PLOG.info("Shiro >> 权限认证");
String username = JWTUtil.getUsername(principalCollection.toString());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获得该用户角色
Integer power = userMapper.getPowerByUsername(username);
Set set = new HashSet<>();
if (power == 100) {
set.add("admin");
}
set.add("user");
info.setRoles(set);
return info;
}
}
ShiroConfig.java
从这里加上我们上文写过的JWTFilter 以及配置一下权限控制
@Configuration
public class ShiroConfig {
private static final Logger PLOG = LoggerFactory.getLogger(ShiroConfig.class);
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 自定义过滤器
Map filterMap = new HashMap<>();
filterMap.put("jwt", new JWTFilter());
shiroFilterFactoryBean.setFilters(filterMap);
shiroFilterFactoryBean.setLoginUrl("/user/ign/notLogin");
shiroFilterFactoryBean.setUnauthorizedUrl("/user/ign/notRole");
// 设置拦截器
Map filterChainDefinitionMap = new LinkedHashMap<>();
// 开放登录、未登录等映射
filterChainDefinitionMap.put("/user/ign/**", "anon");
// 拦截接口
filterChainDefinitionMap.put("/user/**", "jwt");
// 其余接口一律拦截
// filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
PLOG.info("Shiro >> Shiro拦截器工厂类注入成功");
return shiroFilterFactoryBean;
}
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置 realm.
securityManager.setRealm(customRealm());
// 关闭 shiro 自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 自定义身份认证 realm;
*
* 必须写这个类,并加上 @Bean 注解,目的是注入 CustomRealm,
* 否则会影响 CustomRealm类 中其他类的依赖注入
*/
@Bean
public CustomRealm customRealm() {
return new CustomRealm();
}
/**
* 下面的代码是添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
4. 登录
登录的Controller 中,生成token并返给前端使用
@PostMapping(value = "/ign/login")
public ServerResponse login( @RequestBody User user) {
User sourceUser = userService.getUserByUsername(user.getUsername());
String md5Pass = Analysis.knoveMD5(user.getPassword());
if (sourceUser.getPassword().equals(md5Pass)) {
PLOG.info("UserController >> login · 获取Token");
return ServerResponse.createBySuccess(JWTUtil.sign(user.getUsername(), md5Pass));
}
return ServerResponse.createByError("登录失败!");
}