用户登录:
经过shiro中已配置的JwtFilter,执行onAccessDenied方法,判断没有jwt,放行,访问登录接口,进行查询数据库进行用户名密码校验,生成jwt并响应给客户端,返回ResultBody,至此登录执行完毕。
用户未登录访问其他接口:
经过shiro中已配置的JwtFilter,执行onAccessDenied方法,判断没有jwt,放行,访问公共接口成功,访问受限接口失败。
用户已登录访问其他接口:
用户携带jwt,经过shiro中已配置的JwtFilter,执行onAccessDenied方法,判断有jwt,判断jwt是否过期,过期抛出异常,否则交给shiro执行登录,由UserRealm完成用户校验,校验通过则认证成功,即可访问其他接口。
导入shiro-redis的starter包:还有jwt的工具包
<dependency>
<groupId>org.crazycakegroupId>
<artifactId>shiro-redis-spring-boot-starterartifactId>
<version>3.2.1version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
shiro默认supports的是UsernamePasswordToken,而我们现在采用了jwt的方式,所以这里我们自定义一个JwtToken,来完成shiro的supports方法。
public class JwtToken implements AuthenticationToken {
private String jwt;
public JwtToken(String jwt) {
this.jwt = jwt;
}
/**
* -这里getPrincipal和getCredentials统一返回jwt串
* -因为在后面的自定义realm中我们封装进SimpleAuthenticationInfo的principal是vo对象,credentials是jwt串
*/
@Override
public Object getPrincipal() {
return jwt;
}
@Override
public Object getCredentials() {
return jwt;
}
}
这里我们继承的是Shiro内置的AuthenticatingFilter,一个可以内置了可以自动登录方法的的过滤器,继承BasicHttpAuthenticationFilter也是可以的。
@Component
public class JwtFilter extends AuthenticatingFilter {
@Autowired
private JwtUtils jwtUtils;
/**
* -实现登录,生成自定义支持的jwt_token
*/
@Override
protected AuthenticationToken createToken(ServletRequest req, ServletResponse res) throws Exception {
HttpServletRequest request = (HttpServletRequest) req;
//从请求头中获取用户携带的jwt
String jwt = request.getHeader("Authorization");
if(StringUtils.isEmpty(jwt)) {
return null;
}
//作为形参传到自定义realm中的doGetAuthenticationInfo方法
return new JwtToken(jwt);
}
/**
* -不是登录请求的时候,isAccessAllowed方法返回false,会执行该方法
*/
@Override
protected boolean onAccessDenied(ServletRequest req, ServletResponse res) throws Exception {
HttpServletRequest request = (HttpServletRequest) req;
//从请求头中获取用户携带的token
String jwt = request.getHeader("Authorization");
//这里我们不对没有jwt的用户拦截,没有携带jwt,说明是游客身份访问,则不需要交给shiro,而是交给控制器注解拦截
if(StringUtils.isEmpty(jwt)) {
return true;
}else {
//校验jwt是否过期
Claims claims = jwtUtils.getClaimsByJwt(jwt);
if(ObjectUtils.isEmpty(claims)||jwtUtils.isExpired(claims.getExpiration())) {
throw new ExpiredCredentialsException("token已过期,请重新登录");
}
//交给shiro执行登录,其实每次用户每次访问资源都相当于作了自动登录
return executeLogin(req,res);
}
}
/**
* -登录方法异常405时执行该方法
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest req,ServletResponse res){
HttpServletResponse response = (HttpServletResponse) res;
Throwable throwable = e.getCause() == null ? e : e.getCause();
//封装错误信息的结果体
ResultBody resultBody = ResultBody.error(405,throwable.getMessage());
try {
String json = new ObjectMapper().writeValueAsString(resultBody);
//把结果体写入响应流
response.getWriter().write(json);
} catch (Exception exception) {
exception.printStackTrace();
}
return false;
}
/**
* -执行任何方法前先经过此方法,解决跨域
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(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"));
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
# jwt配置信息
jwt.config.secret=liangzhankai
jwt.config.header=Authorization
jwt.config.expire=604800
@Data
@Component
@ConfigurationProperties(prefix = "jwt.config")
public class JwtUtils {
private String secret;
private long expire;
private String header;
/**
* -根据用户id生成jwt_token
*/
public String generateToken(String userId) {
//当前时间
Date nowDate = new Date();
//过期时间,7天
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT") //header,设置类型为JWT
.setSubject(userId) //payload标准中注册的声明,存放用户id,作为用户的唯一标志。
.setIssuedAt(nowDate) //payload标准中注册的声明,设置jwt发行时间
.setExpiration(expireDate) //payload标准中注册的声明,设置过期时间
.signWith(SignatureAlgorithm.HS512, secret) //header,设置加密算法和密钥
.compact(); //压缩为xxx.xxx.xxx这样的jwt串
}
/**
* -从jwt中获得Claims,Claims在JWT原始内容中是一个JSON格式的字符串,其中单个Claim是K-V结构
* @param token
* @return
*/
public Claims getClaimsByJwt(String jwt) {
try {
return Jwts.parser() //获得默认jwt解析器,注意:如果jwt已经过期了,这里会抛出jwt过期异常。
.setSigningKey(secret) //设置密钥解密
.parseClaimsJws(jwt) //设置需要解析的jwt
.getBody();
} catch (ExpiredJwtException e) {
e.printStackTrace();
} catch (UnsupportedJwtException e) {
e.printStackTrace();
} catch (MalformedJwtException e) {
e.printStackTrace();
} catch (SignatureException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
return null;
}
/**
* token是否过期
* @return true:过期
*/
public boolean isExpired(Date expiration) {
//如果过期时间在当前时间之前,说明已经过期
return expiration.before(new Date());
}
}
@Data
@Accessors(chain=true)
public class UserVO {
private String id;
@NotBlank(message="用户名不能为空")
private String username;
@NotBlank
@Email(message="邮箱格式错误")
private String email;
private String avatar;
}
@Data
@Accessors(chain = true)
public class UserDTO implements Serializable{
@NotBlank(message="用户名不能为空")
private String username;
@NotBlank(message="密码不能为空")
private String password;
}
其实这里可以不用VO,直接传输DTO也可
注意:这里使用了实体校验的注解,需要引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-validationartifactId>
dependency>
@Component
public class AccountRealm extends AuthorizingRealm {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserService userService;
/**
* -查询数据库,将获取到用户的角色及权限信息返回
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取主身份信息,即我们在SimpleAuthenticationInfo中封装的 userVO
UserVO userVO = (UserVO) principals.getPrimaryPrincipal();
//获取数据库中的 user
User userDB = userService.findByUsername(userVO.getUsername());
//如果角色集合的第一个元素不为空
if(!ObjectUtils.isEmpty(roles.get(0))) {
//创建权限信息对象
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//遍历roles集合,把角色名依次添加进权限信息对象
roles.forEach(role->{
simpleAuthorizationInfo.addRole(role.getName());
//获取角色对应的权限字符串集合
List<Permission> permissions = permissionService.listPermissionByRoleId(role.getId());
System.out.println(permissions+"...................");
//遍历权限字符串集合,添加进权限信息对象
permissions.forEach(permission->{
simpleAuthorizationInfo.addStringPermission(permission.getName());
});
});
return simpleAuthorizationInfo;
}
return null;
}
/**
* -根据token获得身份信息查询数据库作身份认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//把token强转成自定义的jwt_token
JwtToken jwtToken = (JwtToken) token;
//取出jwt串
String jwt = (String) jwtToken.getPrincipal();
//使用jwt工具类获得claims中的claim的userId
String userId = jwtUtils.getClaimsByJwt(jwt).getSubject();
//查询数据库获取用户
User userDB = userService.findById(userId);
//判断用户异常状态,抛出异常
if(userDB==null) {
throw new UnknownAccountException("用户不存在!");
}
if(userDB.getStatus()==-1) {
throw new LockedAccountException("用户已被禁用!");
}
//把账户非敏感信息封装成vo,作为 principal
UserVO userVO = new UserVO()
.setId(userDB.getId())
.setUsername(userDB.getUsername())
.setAvatar(userDB.getAvatar())
.setEmail(userDB.getEmail());
/**
* -返回认证信息,参数1:principal身份信息 参数2:credentials凭证信息 参数3:realmName
* -shiro最后从jwtToken和SimpleAuthenticationInfo实例中分别调用各自的getCredentials()拿到credentials进行校验
*/
return new SimpleAuthenticationInfo(userVO,jwtToken.getCredentials(),this.getName());
}
/**
* -指定当前realm只处理自定义的jwt_token
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
}
@Configuration
public class ShiroConfig {
@Autowired
private JwtFilter jwtFilter;
/**
* -引入RedisSessionDAO和RedisCacheManager,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享
* @param redisSessionDAO
* @return
*/
@Bean
public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO);
return sessionManager;
}
/**
* -重写了SessionManager和DefaultWebSecurityManager,
* -同时在DefaultWebSecurityManager中为了关闭shiro自带的session方式,需要设置为false,
* -这样用户就不再能通过session方式登录shiro,后面将采用jwt凭证登录。
* @param accountRealm,自定义realm
* @param sessionManager
* @param redisCacheManager
* @return
*/
@Bean
public DefaultWebSecurityManager securityManager(Realm realm,SessionManager sessionManager,RedisCacheManager redisCacheManager){
//创建安全管理器
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);
//在安全管理器中注入会话管理器和redis缓存管理器
securityManager.setSessionManager(sessionManager);
securityManager.setCacheManager(redisCacheManager);
//关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* -设置受限资源与过滤器映射的对应关系
* @return
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition filterChainDefinition = new DefaultShiroFilterChainDefinition();
//创建受限资源映射
Map<String, String> filterChainMap = new LinkedHashMap<>();
//所有资源都要经过名为jwt映射的过滤器
filterChainMap.put("/**", "jwt");
//把受限资源映射加入过滤器链定义
filterChainDefinition.addPathDefinitions(filterChainMap);
return filterChainDefinition;
}
/**
* -过滤器工厂
* @param securityManager
* @param shiroFilterChainDefinition
* @return
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
//创建shiro过滤器工厂
ShiroFilterFactoryBean shiroFilterFactory = new ShiroFilterFactoryBean();
//在shiro中注入安全管理器
shiroFilterFactory.setSecurityManager(securityManager);
//创建jwt映射的jwtFilter并加入shiroFilterFactory
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", jwtFilter);
shiroFilterFactory.setFilters(filters);
//获得受限资源与过滤器映射关系,如 /**:"jwt"
Map<String, String> filterChainDefinition = shiroFilterChainDefinition.getFilterChainMap();
//把受限资源与过滤器映射的对应关系注入shiro过滤器工厂
shiroFilterFactory.setFilterChainDefinitionMap(filterChainDefinition);
return shiroFilterFactory;
}
}
上面的配置,我们主要做了这几件事:
1.引入RedisSessionDAO和RedisCacheManager,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享。
2.重写了SessionManager和DefaultWebSecurityManager,同时在DefaultWebSecurityManager中为了关闭shiro自带的session方式,我们需要设置为false,这样用户就不再能通过session方式登录shiro。后面将采用jwt凭证登录。
3.在ShiroFilterChainDefinition中,我们不再通过编码形式拦截Controller访问路径,而是所有的路由都需要经过JwtFilter这个过滤器,然后判断请求头中是否含有jwt的信息,有就登录,没有就跳过。跳过之后,有Controller中的shiro注解进行再次拦截,比如@RequiresAuthentication,这样控制权限访问。
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}
@PostMapping("/login")
public ResultBody login(@RequestBody @Validated UserDTO userDTO,HttpServletResponse response) {
//根据传过来的用户名查询数据库
User userDB = userService.findByUsername(userDTO.getUsername());
//校验用户
if(userDB!=null) {
//根据用户名生成盐
ByteSource salt = ByteSource.Util.bytes(userDTO.getUsername());
//对密码使用 md5散列1024次+盐 加密
Md5Hash md5Hash = new Md5Hash(userDTO.getPassword(),salt,1024);
//比较密码
if(!md5Hash.toHex().equals(userDB.getPassword())) {
//抛出密码不正确异常
throw new CredentialsException("密码错误!");
}
}else {
//抛出用户不存在异常
throw new UnknownAccountException("用户不存在!");
}
//生成jwt
String jwt = jwtUtils.generateToken(userDB.getId());
//把jwt封装在响应头返回给客户端
response.setHeader("Authorization", jwt);
response.setHeader("Access-control-Expose-Headers", "Authorization");
//封装vo对象传给前端
UserVO userVO = new UserVO()
.setId(userDB.getId())
.setUsername(userDB.getUsername())
.setAvatar(userDB.getAvatar())
.setEmail(userDB.getEmail());
return ResultBody.success(CommonEnum.SUCCESS,userVO);
}
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state:{
token:localStorage.getItem('token'),
userInfo:JSON.parse(localStorage.getItem('userInfo'))
},
mutations:{
//对state定义的变量进行操作
SET_TOKEN:(state,token)=>{
state.token = token;
localStorage.setItem('token',token);
},
SET_USERINFO:(state,userInfo)=>{
state.userInfo = userInfo;
localStorage.setItem('userInfo',JSON.stringify(userInfo));
},
REMOVE_TOKEN:(state,token)=>{
state.token='';
localStorage.removeItem('token');
},
REMOVE_USERINFO:(state,userInfo)=>{
state.userInfo={};
localStorage.removeItem('userInfo');
}
},
getters:{
//获取state里的变量
getUserInfo:state=>{
return state.userInfo
},
getToken:state=>{
return state.token;
}
},
actions:{
},
modules:{
}
})
import axios from 'axios'
import ElementUI from 'element-ui'
import router from './router'
import store from './store/index.js'
// 请求url的前缀
axios.defaults.baseURL = 'http://localhost:8081'
// 前置拦截
axios.interceptors.request.use(config=>{
//携带token
let token = localStorage.getItem('token');
if(token){
//每次请求都携带token
config.headers['Authorization'] = token;
}
return config;
})
//后置拦截
axios.interceptors.response.use(response=>{
let res = response.data;
if(res.code == 400){
//出现shiro异常,跳转到登录页面
ElementUI.Message.error(res.msg);
router.push({name:'Login'});
//阻止后续登录成功逻辑的代码
return Promise.reject(res.msg);
}else if(res.code >= 401 && res.code < 500){
ElementUI.Message.error(res.msg);
//阻止后续登录成功逻辑的代码
return Promise.reject(res.msg);
}else{
//正常返回
return response;
}
})
import router from './router'
import ElementUI from 'element-ui'
// 路由判断登录,根据路由器配置文件的参数
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requireAuth)) { //需要权限的路由
const token = localStorage.getItem("token");
if (!token) { //没有token
ElementUI.Message.error('权限不足,请先登录');
next({ //跳转到登录页
path:'/login'
})
}
} else { //不需要权限的路由
next();
}
})