目录
注:该项目中没有使用到oauth2流程。JWT采用的是无状态。
1、pom.xml
<!--Apache Shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
2、config
2.1、JwtConfig
@Configuration
@ConfigurationProperties(prefix = "bm.dataservice.jwt")
public class JwtConfig {
/**
* JWT 过期时间
*/
private Long expire;
/**
* JWT secret
*/
private String secret;
public Long getExpire() {
return expire;
}
public void setExpire(Long expire) {
this.expire = expire;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
}
2.2、ShiroConfig
@Configuration
public class ShiroConfig {
/**
* 登录页
*/
@Value("${bm.dataservice.page.login}")
private String loginPage;
/**
* 首页
*/
@Value("${bm.dataservice.page.index}")
private String indexPage;
/**
* Title: getManager
* Description: securityManager
* @param realm
* @return
* DefaultWebSecurityManager
*/
@Bean("securityManager")
public DefaultWebSecurityManager getManager(JwtRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 使用自己的realm
manager.setRealm(realm);
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-
* StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
/**
* Title: factory
* Description: shiroFilter
* @param securityManager
* @return
* ShiroFilterFactoryBean
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<>(16);
filterMap.put("JwtAuth", new JwtFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
factoryBean.setUnauthorizedUrl(loginPage);
factoryBean.setLoginUrl(loginPage);
// SuccessUrl其实在本项目中没有用,本项目中是Controller直接返回Json对象,不涉及到页面跳转
factoryBean.setSuccessUrl(indexPage);
/*
* 自定义url规则 http://shiro.apache.org/web.html#urls-
*/
Map<String, String> filterRuleMap = new HashMap<String, String>(10);
// 所有请求通过我们自己的JWT Filter
filterRuleMap.put("/auth/login", "anon");
filterRuleMap.put("/auth/test", "JwtAuth");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
* Title: lifecycleBeanPostProcessor
* Description: 配置 Bean 后置处理器: 会自动的调用和 Spring 整合后各个组件的生命周期方法
* @return
* LifecycleBeanPostProcessor
* 【坑】:为什么此处加static :解决@Value 都不到数据的问题
* https://blog.csdn.net/wuxuyang_7788/article/details/70141812
*/
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
*
* Title: defaultAdvisorAutoProxyCreator
* Description: 1.下面的代码是添加注解支持
* @return
* DefaultAdvisorAutoProxyCreator
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* Title: authorizationAttributeSourceAdvisor
* Description: 2.下面的代码是添加注解支持
* @param securityManager
* @return
* AuthorizationAttributeSourceAdvisor
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
3、JwtRealm
先建JwtToken
public class JwtToken implements AuthenticationToken {
/**
* @Fields serialVersionUID :
*/
private static final long serialVersionUID = -4544643660260209460L;
/**
* 密钥
*/
private String token;
/**
* JWTToken
* @param token
*/
public JwtToken(String token) {
this.token = token;
}
/**
* Principal
*/
@Override
public Object getPrincipal() {
return token;
}
/**
* Credentials
*/
@Override
public Object getCredentials() {
return token;
}
}
然后
@Component
public class JwtRealm extends AuthorizingRealm {
/**
* Jwt配置
*/
@Autowired
private JwtConfig jwtConfig;
/**
* 大坑!,必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// Auto-generated method stub
return null;
}
/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
boolean verify = JwtUtil.verify(token,jwtConfig.getSecret());
if (!verify) {
throw new AuthenticationException("JWT Token 验证失败!");
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token, token, this.getName());
return simpleAuthenticationInfo;
}
}
4、JwtFilter
public class JwtFilter extends UserFilter {
/**
*
* Description: 跨域处理
* @param servletRequest
* @param servletResponse
* @return
* @throws Exception
*/
@Override
protected boolean preHandle(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String header = request.getHeader("Origin");
if (header == null) {
header = "*";
}
// 允许向该服务器提交请求的URI,*表示全部允许
// 在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin,
// 但是shiro中不能设成*,要指定路径
response.setHeader("Access-Control-Allow-Origin", header);
// 允许提交请求的方法,*表示全部允许
response.setHeader("Access-Control-Allow-Methods", request.getMethod());
// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
response.setHeader("Access-Control-Max-Age", "3600");
// 允许访问的头信息
response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
// 允许跨域,在做登录校验的时候有用
response.setHeader("Access-Control-Allow-Credentials", "true");
if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
// 预检,直接跳出
response.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* Description: 请求Filter
* @param servletRequest
* @param servletResponse
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse,
Object mappedValue) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
System.out.println("进来啦,开始进行token验证");
// 1. 请求头无Authorization则禁止访问
String authorization = request.getHeader("Authorization");
if (null == authorization) {
return false;
}
// 2. 请求头 token为空则禁止访问
String token = authorization.substring(authorization.indexOf("Bearer") + 6).trim();
if (token.length() == 0) {
return false;
}
//3. token检验,到realm中进行校验
JwtToken jwtToken = new JwtToken(token);
Subject subject = getSubject(request, response);
try {
//----------------------------start
//校验直接用isAuthenticated(),如果失败再进行subject.login(jwtToken);
//该段实在写博客的时候突然想到的,项目中还没改
subject.isAuthenticated();
//-----------------------------end
subject.login(jwtToken);
} catch (Exception e) {
return false;
}
return true;
}
}
上面进行用户时候进行过认证,应该直接用subject.isAuthenticated();,如果没有则进行subject.login(jwtToken);该段实在写博客的时候突然想到的,项目中还没改。
在shiroConfig中应用:
5、controller
@PostMapping("/login")
public CommonResult login(@ApiIgnore HttpServletRequest request, @ApiIgnore HttpServletResponse response,
String username, String password, String courtCode) {
Map<String, Object> userInfoMap;
Map<String, Object> map = new HashMap<>(2);
try {
SysUser userInfo = organizationUserService.querylogin(username, password, courtCode);
if (null == userInfo) {
return CommonResult.build(500, "用户名或密码错误");
}
userInfoMap = organizationUserService.queryUserInfo(userInfo.getId());
// 1.用户名密码登录逻辑
LoginInfo loginInfo = new LoginInfo();
loginInfo.setId(userInfo.getId());
loginInfo.setUserName(userInfo.getUsername());
loginInfo.setName(userInfo.getName());
loginInfo.setRole(userInfoMap.get("role").toString());
loginInfo.setDepartmentId(userInfo.getDepartmentId());
loginInfo.setDqdm(userInfoMap.get("dqdm").toString());
loginInfo.setRoleIdList((List<String>) userInfoMap.get("roleId"));
//用户信息未绑定 登陆失败
if(CollectionUtils.isEmpty(loginInfo.getRoleIdList())){
return CommonResult.build(500, "该用户暂时未绑定角色请与系统管理员联系!");
}
// 2.证书签发
String secret = jwtConfig.getSecret();
Long expire = jwtConfig.getExpire();
String json = JsonUtils.objectToJson(loginInfo);
String token = JwtUtil.sign(secret, expire, json);
// 3.token写入cookie。web开发该部分可由后端实现也可由前端实现;app开发只能前端写入storage
map.put("token", token);
}catch (Exception e){
e.printStackTrace();
return CommonResult.build(500, "无法获取登录信息");
}
return CommonResult.ok(map);
}
6、补充-JwtUtils
public class JwtUtil {
/**
* Title: verify
* Description: 校验token是否正确
*
* @param token 密钥
* @param loginfo 用户信息
* @param secret 私钥
* @return boolean
*/
public static boolean verify(String token, String loginfo, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = null;
if (loginfo.length() > 0) {
verifier = JWT.require(algorithm).withClaim("claim", loginfo).build();
} else {
verifier = JWT.require(algorithm).build();
}
verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}
/**
* Title: verify
* Description: 不帶Claim,只做secret校验
*
* @param token
* @param secret
* @return boolean
*/
public static boolean verify(String token, String secret) {
return verify(token, "", secret);
}
/**
* Title: getLoginInfo
* Description: 取用戶信息 ,无需secret解密也能获得
*
* @param token
* @return LoginInfo
*/
public static LoginInfo getLoginInfo(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
String info = jwt.getClaim("claim").asString();
if (info.trim().length() > 0) {
return JsonUtils.jsonToPojo(info, LoginInfo.class);
} else {
return null;
}
} catch (JWTDecodeException e) {
return null;
}
}
/**
* Title: getLoginInfoFromCookie
* Description: 从Cookie中取用户信息
*
* @return LoginInfo
*/
public static LoginInfo getLoginInfoFromCookie() {
/*HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
String token = CookieUtils.getCookieValue(request, "token");
if (!StringUtils.isNotEmpty(token)) {
return null;
}
LoginInfo loginInfo = getLoginInfo(token);
return loginInfo;*/
//原來是从Cookie中取用户信息,现在直接从header中读取用户信息
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
String authorization = request.getHeader("Authorization");
if (null == authorization) {
return null;
}
String token = authorization.substring(authorization.indexOf("Bearer") + 6).trim();
if (!StringUtils.isNotEmpty(token)) {
return null;
}
LoginInfo loginInfo = getLoginInfo(token);
return loginInfo;
}
/**
* Title: sign
* Description: 生成签名
*
* @param secret 密钥
* @param expire 过期时间
* @param loginfo 用户信息
* @return String
*/
public static String sign(String secret, Long expire, String loginfo) {
try {
Date date = new Date(System.currentTimeMillis() + expire);
Algorithm algorithm = Algorithm.HMAC256(secret);
return JWT.create().withClaim("claim", loginfo).withExpiresAt(date).sign(algorithm);
} catch (UnsupportedEncodingException e) {
return null;
}
}
}