前端用户未登录仅可访问登录页面和主界面,后端过滤器放行图片,jar,登录注册等接口,通过login方法验证用户,返回登录成功后的token信息并且存储在redis中。前端每次发起的请求都需要在http头部携带上token用来验证身份,并且设置统一的返回结果处理token失效以及权限不足
重写BasicHttpAuthenticationFilter,重写PreHandle方法(跨域设置,因为http头部携带了authorization所以会发起两次请求,应当直接放行第一次options方法请求,验证token是否为空),执行super.executeLogin方法(获取createToken中的自定义token,并且执行subject.login(token)方法,调用自定义realm的doAuthentication方法),如果验证成功,那么执行重写的onLoginSuccess(验证token是否过期),如果验证事变,执行重写的onLoginFailure(处理登录的异常)。最后自定义一个方法返回一个vo类。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.1.tmpversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.13version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>3.3.1.tmpversion>
dependency>
<dependency>
<groupId>org.apache.velocitygroupId>
<artifactId>velocity-engine-coreartifactId>
<version>2.2version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.73version>
dependency>
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.8.2version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.4.0version>
dependency>
#mysql8的配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/wlw?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=
spring.redis.port=6379
spring.redis.host=localhost
public class CustomRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("shiro授权");
//获取用户名
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
String username = TokenUtil.getUser(PrincipalCollection.getPrincipal().toString())
//根据用户名从自己的数据库中获取role和permission信息
return info;
}
/**
* 登录验证
* 因为token是jwt签名的,所以可以利用jwt的verify方法验证,而不需要查找数据库
* @param authenticationToken token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("shiro验证");
String principal = authenticationToken.getPrincipal().toString();//获取token
String userName = TokenUtil.getUser(principal);
User user = null;
//从自己的数据判断查找user
if(user!=null){
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal,principal,getName());
return info;
}
return null;
}
/**
* 将自定义的realm需要支持自定义的token,必须重写方法,不然运行时会出错。
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token){
return token != null && token instanceof JWTToken;
}
}
public class JWTFilter extends BasicHttpAuthenticationFilter {
private RedisUtil redisUtil;
public RedisUtil getRedisUtil() {
return redisUtil;
}
public void setRedisUtil(RedisUtil redisUtil) {
this.redisUtil = redisUtil;
}
/**
* 判断是否允许通过
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
System.out.println("执行登录方法");
try{
return executeLogin(request,response);
}catch (Exception e){
//登录时失败,也就是用户不存在
//responseError(response,"token出现异常",445);
e.printStackTrace();
return false;
}
}
/**
* 是否进行登录请求,判断authorization是否为空,也就是是否携带token信息
* @param request
* @param response
* @return
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
//String token=((HttpServletRequest)request).getHeader("Authorization");
HttpServletRequest servletRequest = (HttpServletRequest) request;
String token = servletRequest.getHeader("Authorization");
/* Enumeration headers = ((HttpServletRequest) request).getHeaderNames();
while(headers.hasMoreElements()){
String header = headers.nextElement();
System.out.println(header+":"+servletRequest.getHeader(header));
}*/
//System.out.println(token);
if (StringUtils.isAnyBlank(token)){
return false;
}
return true;
}
/**
* 创建shiro token
* @param request
* @param response
* @return
*/
@Override
protected JWTToken createToken(ServletRequest request, ServletResponse response) {
System.out.println("创建token");
//String jwtToken = ((HttpServletRequest)request).getHeader("token");
String jwtToken = ((HttpServletRequest)request).getHeader("Authorization");
if(jwtToken!=null) return new JWTToken(jwtToken);
return null;
}
/**
* isAccessAllowed为false时调用,验证失败
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
/* System.out.println("token登录失败");
this.sendChallenge(request,response);
responseError(response,"token登录失败",445);*/
return false;
}
/**
* shiro验证成功调用
* 已经执行了登入方法,token验证成功
* @param token
* @param subject
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
//System.out.println("token登录成功,验证过期时间");
String jwttoken= (String) token.getPrincipal();
//这里已经执行
//这里的token经过executeLogin的验证,肯定是不为空的
//检验token是否正确
long expireTime = redisUtil.getExpire(TokenUtil.getUser(jwttoken));
//System.out.println(expireTime);
if(expireTime>0) {
//System.out.println("验证成功,允许访问");
return true;//验证成功
}
responseError(response,"token失效",444);
return false;//验证失
}
/**
* 拦截器的前置方法,此处进行跨域处理
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
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"));
httpServletResponse.setHeader("Access-Control-Allow-Credentials","true");
//解决前端发起两次请求,第一次请求方式是预请求,不会携带token,
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())){
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;//预请求不做处理
}
//System.out.println(httpServletRequest.getHeader("Authorization"));
//如果不带token,不去验证shiro
if (!isLoginAttempt(request,response)){
responseError(response,"token不存在,未登录",446);//未登录
return false;
}
//System.out.println("token存在");
return super.preHandle(request,response);
}
/**
* 登录操作时,出现异常
* @param token
* @param e
* @param request
* @param response
* @return
*/
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
//登录时出现异常
System.out.println("登录时出现异常");
if(e instanceof UnknownAccountException) responseError(response,"token不正确",445);
return false;
}
/**
* 验证失败
* @param response
* @param msg
*/
private void responseError(ServletResponse response,String msg,Integer code){
//System.out.println("返回错误信息");
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(200);
httpResponse.setCharacterEncoding("UTF-8");
Result result = new Result(msg).NO(code);
String jsonStr = JSON.toJSONString(result);//需要标注get方法
//System.out.println(result);
httpResponse.setContentType("application/json;charset=UTF-8");
try {
httpResponse.getWriter().print(jsonStr);
} catch (IOException e) {
e.printStackTrace();
System.out.println("返回错误失败");
}
}
}
@Configuration
public class ShiroConfig {
/**
* 这里的JWTFilter不能由spring创建,所有资源都会被拦截,必须在创建工厂对象中创建对象
* @param securityManager 安全管理器
* @param redisUtil redis工具类
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,RedisUtil redisUtil){
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
//设置自己的过滤器
Map<String, Filter> filterMap=new LinkedHashMap();
//filterMap.put("jwt", filter);
JWTFilter filter = new JWTFilter();
filter.setRedisUtil(redisUtil);
filterMap.put("jwt", filter);
shiroFilterFactoryBean.setSecurityManager(securityManager);
//不要用HashMap来创建Map,会有某些配置失效,要用链表的LinkedHashmap
Map<String,String> filterRuleMap=new LinkedHashMap<>();
//放行接口
filterRuleMap.put("/","anon");
filterRuleMap.put("/**/pic/**","anon");
filterRuleMap.put("/**/webjars/**","anon");
filterRuleMap.put("/**/login","anon");
filterRuleMap.put("/**/regist","anon");
filterRuleMap.put("/**/css/**","anon");
filterRuleMap.put("/**/images/**","anon");
filterRuleMap.put("/**/js/**","anon");
filterRuleMap.put("/**/lib/**","anon");
//拦截所有接口
filterRuleMap.put("/**","jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
shiroFilterFactoryBean.setFilters(filterMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(CustomRealm customRealm){
//设置自定义Realm
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
securityManager.setRealm(customRealm);
//关闭shiro自带的session
DefaultSubjectDAO subjectDAO=new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator=new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public CustomRealm customRealm(){
CustomRealm customRealm = new CustomRealm();
// HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// credentialsMatcher.setHashIterations(MD5Util.HASH_ITERATIONS);//设置散列次数
// credentialsMatcher.setHashAlgorithmName("MD5");//设置加密方法的名称
// customRealm.setCredentialsMatcher(credentialsMatcher);
return customRealm;
}
/**
* 授权属性源配置
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor=new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
public class MD5Util {
//散列次数
public static final int HASH_ITERATIONS = 2;
//字符串
private static final char [] chars = new String("123456789abcdefghijklmnopqrstuvwxyz!@#$^%").toCharArray();
/**
* 获取加密密码
* @param password 密码
* @param salt 盐
* @return
*/
public static String getSecretPassword(String password,String salt){
return new Md5Hash(password, salt, HASH_ITERATIONS).toString();
}
public static String getRandomSalt(int length){
if(length<=0){throw new RuntimeException("长度不能小于等于0");}
StringBuilder builder = new StringBuilder();
for(int i = 0 ; i<length;i++) builder.append(chars[new Random().nextInt(chars.length)]);
return builder.toString();
}
}
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(@Autowired RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//redisTemplate.setDefaultSerializer(new StringRedisSerializer());设置所有的功能序列化方式吗?
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);//这个类中的某些类不可用
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
redisTemplate.setKeySerializer(stringRedisSerializer);//设置key的序列化方式
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);//设置value(string.list,set,zset)序列化方式
redisTemplate.setHashKeySerializer(stringRedisSerializer);//设置hash的key的序列化方式string.
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);//设置hash的value序列化方式string不能存储对象
redisTemplate.afterPropertiesSet();//?????
return redisTemplate;
}
}
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String,Object> redisTemplate;//工具类的方法是静态,并且工具类需要由spring容器加载
//获取过期时间
public long getExpire(String key){
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
//设置token生命周期
public boolean expire(String key,String value,long seconds){
try{
redisTemplate.opsForValue().set(key,value,seconds,TimeUnit.SECONDS);
}catch(Exception e){
return false;
}
return true;
}
//删除token,用户退出登录
public boolean deleteKey(String key){
return redisTemplate.delete(key);
}
}
public class JWTToken implements AuthenticationToken {
private String principal;
private String credentials;
public JWTToken(){this(null);}
public JWTToken(String principal){
this.principal=principal;
this.credentials=principal;
};
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return this.credentials;
}
}
public class TokenUtil {
private String token;
public static final long EXPIRE_TIME = 5*60*1000;//默认5min;
//private static final long REFRESH_EXPIRE_TIME = 5*60*1000;//refreshtoken到期时间5min;
private static final String TOKEN_SECRET = "fslk432234&*465fds8^&^@*594#";//秘钥
/**
* 对用户信息进行秘钥加密并且返回
* @param user 信息
* @param currentTime 当前时间
* @return 加密后的字符串
*/
public static String sign(String user,long currentTime){
String token = JWT.create()
.withIssuer("ly")//发行人
.withClaim("user",user)//存放数据
.withClaim("currentTime",currentTime)//起始时间
.withExpiresAt(new Date(currentTime+EXPIRE_TIME))//到期时间
.sign(Algorithm.HMAC256(TOKEN_SECRET));//加密
return token;
}
/**
* 验证token是否过期
* @param token 加密的信息
* @return
*/
public static Boolean verify(String token){
//创建token验证器
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("ly").build();
DecodedJWT decodedJWT = jwtVerifier.verify(token);
System.out.println("认证通过");
System.out.println("user:"+decodedJWT.getClaim("user").asString());
System.out.println("过期时间:"+decodedJWT.getExpiresAt().toString());
return true;
}
/***
* 获取token的用户名
* @param token
* @return
*/
public static String getUser(String token){
try{
return JWT.decode(token).getClaim("user").asString();
}catch(Exception e){
System.out.println(e.getMessage());//解析出错
return "error_user15456465";
}
}
/**
* 获取过期时间
* @param token
* @return
*/
public static long getExpireTime(String token){
return JWT.decode(token).getExpiresAt().getTime();
}
}
@Aspect
@Component
public class SysLogAspect {
@Autowired
private ILogService sysLogService;
//定义切点 @Pointcut
//在注解的位置切入代码
@Pointcut("@annotation(日志注解)")
public void logPoinCut() {
}
//切面 配置通知
@AfterReturning("logPoinCut()")
public void saveSysLog(JoinPoint joinPoint) throws UnsupportedEncodingException {
//保存日志
Log sysLog = new Log();
//从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
//获取操作
MyLog myLog = method.getAnnotation(MyLog.class);
if (myLog != null) {
String value = myLog.value();
sysLog.setEvent(value);//保存获取的操作
}
//获取请求的类名
//String className = joinPoint.getTarget().getClass().getName();
//获取请求的方法名
//String methodName = method.getName();
//sysLog.setEvent(className + "." + methodName);
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
String userName = request.getHeader("Authorization");
userName= TokenUtil.getUser(userName);
//将操作的时间,事件,以及操作人存入到数据库中。
}
}
@ControllerAdvice
public class MyExceptionHandler {
/**
* 用户处理权限不足
* @param ex
* @param response
*/
@ExceptionHandler(UnauthorizedException.class)
public void unauthorizedException(Exception ex,HttpServletResponse response){
System.out.println(response);
try {
response.setContentType("application/json,utf-8");//设置前端解析数据的类型;
response.getWriter().print(JSON.toJSONString(new Result("权限不足").NO(447)));//权限不足
} catch (IOException e) {
e.printStackTrace();
}
}
@ExceptionHandler(NullPointerException.class)
public void nullPointExcpetion(Exception e){
e.printStackTrace();
}
@ExceptionHandler(Exception.class)
public void exception(Exception e, HttpServletResponse response){
try {
response.getWriter().print(e.getMessage());
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
public class Result {
private long resultCode;//错误编码
private String msg;//返回描述
private boolean success;//是否通过
private Object res;//返回的数据
public Result OK(){
this.resultCode=200;
this.success=true;
return this;
}
public Result NO(){return NO(400);}
public Result NO(long code){
this.success=false;
this.resultCode=code;
return this;
}
public Result(String msg,Object res){
this.msg=msg;
this.res=res;
}
public Result(String msg){this(msg,null);}
@Override
public String toString() {
return "{" +
"code=" + resultCode +
", msg='" + msg + '\'' +
", success=" + success +
", res=" + res +
'}';
}
public long getResultCode() {
return resultCode;
}
public void setResultCode(long resultCode) {
this.resultCode = resultCode;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public Object getRes() {
return res;
}
public void setRes(Object res) {
this.res = res;
}
}
@RequestMapping("/login")
public Result login(@RequestBody() User user) {
String token = "";
System.out.println("登录");
try {
User u = userService.getUserByName(user.getName());
String passwordInDB = u.getPassword();
String salt = u.getSalt();
String passwordEncoded = MD5Util.getSecretPassword(user.getPassword(),salt);
if(!passwordEncoded.equals(passwordInDB)){
return new Result("密码错误").NO();
}
else {
//获取token返回给前端
long currentTime = System.currentTimeMillis();
token = TokenUtil.sign(user.getName(),currentTime);
System.out.println("登录成功,您的token是:"+token);
//存储到redis缓存中
boolean flag = redisUtil.expire(u.getName(),"userExpire",TokenUtil.EXPIRE_TIME);
if(!flag) System.out.println("redis存储失败");
}
} catch (Exception e) {//获取的用户
if(e instanceof RuntimeException){}//用户名错误
}
return new Result("success",token).OK();
}
前后端分离下,需要设置跨域,如果我们添加了http请求头的信息,那么http会发起两次请求,第一次是options请求用来进行预检的,后端过滤器不能进行拦截,直接返回就就可以了,不然不会发起第二次请求。