前些日子我曾经使用shiro来实现用户的登录,将账号密码托管给shiro,客户端与服务端的连接通过cookie和session,
但是目前使用最多的登录都是无状态的,使用jwt或者oauth来实现登录,所以也特地记录一下。
1.第一步先添加jwt的依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.7.0</version>
</dependency>
2.修改shiro的配置,大体上没有什么大的变化,主要就是关闭session和配置jwt到shiro中
@Bean
public MyShiroRealm myShiroRealm(HashedCredentialsMatcher matcher){
MyShiroRealm myShiroRealm= new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(matcher);
return myShiroRealm;
}
@Bean
public DefaultWebSecurityManager securityManager(HashedCredentialsMatcher matcher){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm(matcher));
/*
* 关闭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);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
//如果没有此name,将会找不到shiroFilter的Bean
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//shiroFilterFactoryBean.setLoginUrl("/login"); //表示指定登录页面 (前后分离不适用)
//shiroFilterFactoryBean.setSuccessUrl("/user/list"); // 登录成功后要跳转的链接 (前后分离不适用)
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();//拦截器, 配置不会被拦截的链接 顺序判断
//filterChainDefinitionMap.put("/login","anon"); //所有匿名用户均可访问到Controller层的该方法下
filterChainDefinitionMap.put("/userLogin","anon");
filterChainDefinitionMap.put("/image/**","anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/fonts/**","anon");
filterChainDefinitionMap.put("/js/**","anon");
filterChainDefinitionMap.put("/logout","logout");
filterChainDefinitionMap.put("/**", "authc"); //authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
//filterChainDefinitionMap.put("/**", "user"); //user表示配置记住我或认证通过可以访问的地址
// 添加自己的过滤器并且取名为jwt
LinkedHashMap<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwt", jwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// 过滤链定义,从上向下顺序执行,一般将放在最为下边
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public JwtFilter jwtFilter() {
return new JwtFilter();
}
/**
* SpringShiroFilter首先注册到spring容器
* 然后被包装成FilterRegistrationBean
* 最后通过FilterRegistrationBean注册到servlet容器
* @return
*/
@Bean
public FilterRegistrationBean delegatingFilterProxy(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
hashedCredentialsMatcher.setHashIterations(1024);// 设置加密次数
return hashedCredentialsMatcher;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(HashedCredentialsMatcher matcher) {//@Qualifier("hashedCredentialsMatcher")
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager(matcher));
return authorizationAttributeSourceAdvisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
3.封装token来替换Shiro原生Token,要实现AuthenticationToken接口
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = -8451637096112402805L;
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
4.添加一个JwtUtil的工具类来操作token
public class JwtUtil {
/**
* 过期时间30分钟
*/
public static final long EXPIRE_TIME = 30 * 60 * 1000;
/**
* 校验token是否正确
* @param token 密钥
* @param secret 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String username, String secret) {
try {
// 根据密码生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
// 效验TOKEN
DecodedJWT jwt = verifier.verify(token);
log.info(jwt+":-token is valid");
return true;
} catch (Exception e) {
log.info("The token is invalid{}",e.getMessage());
return false;
}
}
/**
* 获得token中的信息无需secret解密也能获得
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
log.error("error:{}", e.getMessage());
return null;
}
}
/**
* 生成签名,5min(分钟)后过期
* @param username 用户名
* @param secret 用户的密码
* @return 加密的token
*/
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
}
}
5.写一个拦截器JwtFilter,继承BasicHttpAuthenticationFilter类
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
@Autowired
private RedisUtil redisUtil;
private AntPathMatcher antPathMatcher =new AntPathMatcher();
/**
* 执行登录认证(判断请求头是否带上token)
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
log.info("JwtFilter-->>>isAccessAllowed-Method:init()");
//如果请求头不存在token,则可能是执行登陆操作或是游客状态访问,直接返回true
if (isLoginAttempt(request, response)) {
return true;
}
//如果存在,则进入executeLogin方法执行登入,检查token 是否正确
try {
executeLogin(request, response);return true;
} catch (Exception e) {
throw new AuthenticationException("Token失效请重新登录");
}
}
/**
* 判断用户是否是登入,检测headers里是否包含token字段
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
log.info("JwtFilter-->>>isLoginAttempt-Method:init()");
HttpServletRequest req = (HttpServletRequest) request;
if(antPathMatcher.match("/userLogin",req.getRequestURI())){
return true;
}
String token = req.getHeader(CommonConstant.ACCESS_TOKEN);
if (token == null) {
return false;
}
Object o = redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token);
if(ObjectUtils.isEmpty(o)){
return false;
}
log.info("JwtFilter-->>>isLoginAttempt-Method:返回true");
return true;
}
/**
* 重写AuthenticatingFilter的executeLogin方法丶执行登陆操作
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
log.info("JwtFilter-->>>executeLogin-Method:init()");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(CommonConstant.ACCESS_TOKEN);//Access-Token
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获, 反之则代表登入成功,返回true
getSubject(request, response).login(jwtToken);return true;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
log.info("JwtFilter-->>>preHandle-Method:init()");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
6.修改自定义的Realm
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private RoleService roleService;
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
@Autowired
private RedisUtil redisUtil;
/**
* 必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 访问控制。比如某个用户是否具有某个操作的使用权限
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
User user = (User) principalCollection.getPrimaryPrincipal();if (user == null) {
log.error("授权失败,用户信息为空!!!");
return null;
}
try {
//获取用户角色集
Set<String> listRole= roleService.findRoleByUsername(user.getUserName());
simpleAuthorizationInfo.addRoles(listRole);
//通过角色获取权限集
for (String role : listRole) {
Set<String> permission= permissionService.findPermissionByRole(role);
simpleAuthorizationInfo.addStringPermissions(permission);
}
return simpleAuthorizationInfo;
} catch (Exception e) {
log.error("授权失败,请检查系统内部错误!!!", e);
}
return simpleAuthorizationInfo;
}
/**
* 用户身份识别(登录")
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token = (String) authenticationToken.getCredentials();// 校验token有效性
String username = JwtUtil.getUsername(token);if (Strings.isNullOrEmpty(username)) {
throw new AuthenticationException("token非法无效!");
}// 查询用户信息
User sysUser = userService.selectUserOne(username);
if (sysUser == null) {
throw new AuthenticationException("用户不存在!");
}// 判断用户状态
if (sysUser.getValid()==0) {
throw new AuthenticationException("账号已被禁用,请联系管理员!");
}// 校验token是否超时失效 & 或者账号密码是否错误
if (!jwtTokenRefresh(token, username, sysUser.getPassWord())) {
throw new AuthenticationException("Token失效请重新登录!");
}return new SimpleAuthenticationInfo(sysUser,token,ByteSource.Util.bytes(sysUser.getSalt()),getName());
}
/**
* JWTToken刷新生命周期 (解决用户一直在线操作,提供Token失效问题)
* 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)
* 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
* 3、当该用户这次请求JWTToken值还在生命周期内,则会通过重新PUT的方式k、v都为Token值,缓存中的token值生命周期时间重新计算(这时候k、v值一样)
* 4、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
* 5、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
* 6、每次当返回为true情况下,都会给Response的Header中设置Authorization,该Authorization映射的v为cache对应的v值。
* 7、注:当前端接收到Response的Header中的Authorization值会存储起来,作为以后请求token使用
* 参考方案:https://blog.csdn.net/qq394829044/article/details/82763936
*
* @param userName
* @param passWord
* @return
*/
public boolean jwtTokenRefresh(String token, String userName, String passWord) {
log.info("jwtTokenRefresh参数:token="+token+",userName="+userName+",passWord="+passWord);
String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));if (!Strings.isNullOrEmpty(cacheToken)) {
// 校验token有效性
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
String newAuthorization = JwtUtil.sign(userName, passWord);
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization,JwtUtil.EXPIRE_TIME / 1000);
} else {
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken,JwtUtil.EXPIRE_TIME / 1000);
}
return true;
}
return false;
}
}
7.登录接口修改
public class LoginController {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisUtil redisUtil;
/**
* 登录
* @return
*/
@PostMapping(value = "/userLogin")
@ResponseBody
public Result<JSONObject> toLogin(@RequestBody User loginUser) throws Exception {
Result<JSONObject> result = new Result<>();
String userName = loginUser.getUserName();
String passWord = loginUser.getPassWord();
User user=userMapper.selectUserOne(userName);
if (user == null) {
return result.error500("该用户不存在");
}
if (user.getValid()==0) {
return result.error500("账号已被禁用,请联系管理员!");
} //我的密码是使用uuid作为盐值加密的,所以这里登陆时候还需要做一次对比
SimpleHash simpleHash = new SimpleHash("MD5", passWord, user.getSalt(), 1024);
if(!simpleHash.toHex().equals(user.getPassWord())){
return result.error500("密码不正确");
}
// 生成token
String token = JwtUtil.sign(userName, passWord);
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token,JwtUtil.EXPIRE_TIME / 1000);
JSONObject obj = new JSONObject();
obj.put("token", token);
obj.put("userInfo", user);
result.setResult(obj);
result.success("登录成功");
return result;
}
}
添加的方法,这里的加密算法和加密次数以及盐值都要一致,否则登录时候密码对比会失败
@RequestMapping("/insertUser")
@ResponseBody
public int insertUser(User user){
//将uuid设置为密码盐值
String salt = UUID.randomUUID().toString().replaceAll("-","");
SimpleHash simpleHash = new SimpleHash("MD5", user.getPassWord(), salt, 1024);
user.setPassWord(simpleHash.toHex()).setValid(1).setSalt(salt).setCreateTime(new Date()).setDel(0);
return userMapper.insertSelective(user);
}
定义的常量
public class CommonConstant {
/**
* 删除标志 1 未删除 0
*/
public static final Integer DEL_FLAG_1 = 1;
public static final Integer DEL_FLAG_0 = 0;
public static final Integer SC_INTERNAL_SERVER_ERROR_500 = 500;
public static final Integer SC_OK_200 = 200;
/**
* 访问权限认证未通过 510
*/
public static final Integer SC_JEECG_NO_AUTHZ = 510;
/**
* 登录用户令牌缓存KEY前缀
*/
public static final int TOKEN_EXPIRE_TIME = 3600; //3600秒即是一小时
public static final String PREFIX_USER_TOKEN = "PREFIX_USER_TOKEN_";
/**
* 0:一级菜单
*/
public static final Integer MENU_TYPE_0 = 0;
/**
* 1:子菜单
*/
public static final Integer MENU_TYPE_1 = 1;
/**
* 2:按钮权限
*/
public static final Integer MENU_TYPE_2 = 2;
/**
* 是否用户已被冻结 1(解冻)正常 2冻结
*/
public static final Integer USER_UNFREEZE = 1;
public static final Integer USER_FREEZE = 2;
/**
* token的key
*/
public static String ACCESS_TOKEN = "Access-Token";
/**
* 登录用户规则缓存
*/
public static final String LOGIN_USER_RULES_CACHE = "loginUser_cacheRules";
/**
* 登录用户拥有角色缓存KEY前缀
*/
public static String LOGIN_USER_CACHERULES_ROLE = "loginUser_cacheRules::Roles_";
/**
* 登录用户拥有权限缓存KEY前缀
*/
public static String LOGIN_USER_CACHERULES_PERMISSION = "loginUser_cacheRules::Permissions_";
}
目前只是一个shiro+jwt的简单的登录,第一次登录的时候不需要携带token,登陆之后会返回一个token,然后可以拿着这个token去访问其他接口,
能访问证明成功,后面关于jwt的知识会继续记录,如果你看到,希望能够给我一些建议,作为一个菜鸟,会很感谢你!!!