ITPub博客

首页 > 应用开发 > Java > 备忘录九:Spring Boot+Shiro权限管理

备忘录九:Spring Boot+Shiro权限管理

原创 Java 作者:百联达 时间:2019-10-16 17:48:23 0 删除 编辑

一:配置pom.xml文件

		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>1.4.0</version>
		</dependency>

二:ShiroConfig配置类

@Configuration
public class ShiroConfig {
	@Bean("sessionManager")
	public SessionManager sessionManager() {
		DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
		sessionManager.setGlobalSessionTimeout(24 * 60 * 60 * 1000);
		// 开启会话验证器
		sessionManager.setSessionValidationSchedulerEnabled(true);
		// 删除失效的session
		sessionManager.setDeleteInvalidSessions(true);
		// 指定sessionId,使用默认的“JSESSIONID”
		sessionManager.setSessionIdCookieEnabled(true);
		return sessionManager;
	}
	/** 
	  * 我们在使用shiro的时候,首先都会先初始化SecurityManager,
	  * 然后往SecurityManager中注入shiro的其他组件,像sessionManager、realm等。
	 */
	@Bean("securityManager")
	public SecurityManager securityManager(OAuth2Realm oAuth2Realm, SessionManager sessionManager) {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		securityManager.setRealm(oAuth2Realm);
		securityManager.setSessionManager(sessionManager);
		return securityManager;
	}
	@Bean("shiroFilter")
	public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
		ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
		shiroFilter.setSecurityManager(securityManager);
		// oauth过滤
		Map<String, Filter> filters = new HashMap<>();
		filters.put("oauth2", new OAuth2Filter());
		shiroFilter.setFilters(filters);
		// 配置可以匿名访问的地址,可以根据实际情况自己添加,放行一些静态资源等,anon 表示放行
		Map<String, String> filterMap = new LinkedHashMap<>();
		filterMap.put("/webjars/**", "anon");
		filterMap.put("/druid/**", "anon");
		filterMap.put("/app/**", "anon");
		filterMap.put("/file/**", "anon");
		filterMap.put("/shiro/login", "anon");
		filterMap.put("/swagger/**", "anon");
		filterMap.put("/v2/api-docs", "anon");
		filterMap.put("/swagger-ui.html", "anon");
		filterMap.put("/swagger-resources/**", "anon");
		filterMap.put("/captcha.jpg", "anon");
		// 其它通过自定义的OAuth2Filter进行过滤
		filterMap.put("/**", "oauth2");
		shiroFilter.setFilterChainDefinitionMap(filterMap);
		return shiroFilter;
	}
	@Bean("lifecycleBeanPostProcessor")
	public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
		return new LifecycleBeanPostProcessor();
	}
	/**
	  * @Title: defaultAdvisorAutoProxyCreator
	  * @Description: 扫描上下文,寻找所有的Advistor(通知器),将这些Advisor应用到所有符合切入点的Bean中
	  * @return   Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
	  * @return DefaultAdvisorAutoProxyCreator    返回类型
	  * @throws
	  */
//	@Bean
//	public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
//		DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
//		proxyCreator.setProxyTargetClass(true);
//		return proxyCreator;
//	}
	/**
	  * @Title: authorizationAttributeSourceAdvisor
	  * @Description: 开启shiro aop注解支持
	  * @param securityManager
	  * @return   参数说明
	  * @return AuthorizationAttributeSourceAdvisor    返回类型
	  * @throws
	  */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
		AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
		advisor.setSecurityManager(securityManager);
		return advisor;
	}
}

其中,如果启用了DefaultAdvisorAutoProxyCreator的话,会导致二次代理的问题,Realm中的doGetAuthorizationInfo会重复调用2次。

三:自定义Shiro Filter类OAuth2Filter

public class OAuth2Filter extends AuthenticatingFilter {
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token
        String token = getRequestToken((HttpServletRequest) request);
        if(StringUtils.isNullOrEmpty(token)){
            return null;
        }
        return new OAuth2Token(token);
    }
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //现在vue项目中使用axios发送http请求,每次请求都会多一次Request Method: OPTIONS请求,称为“预检”请求
    	if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){
            return true;
        }
        return false;
    }
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token,如果token不存在,直接返回401
        String token = getRequestToken((HttpServletRequest) request);
        if(StringUtils.isNullOrEmpty(token)){
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
            String json = new Gson().toJson(R.error(HttpStatus.SC_UNAUTHORIZED, "invalid token"));
            httpResponse.getWriter().print(json);
            return false;
        }
        return executeLogin(request, response);
    }
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            R r = R.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage());
            String json = new Gson().toJson(r);
            httpResponse.getWriter().print(json);
        } catch (IOException e1) {
        }
        return false;
    }
    /**
     * 获取请求的token
     */
    private String getRequestToken(HttpServletRequest httpRequest){
        //从header中获取token
        String token = httpRequest.getHeader("token");
        //如果header中不存在token,则从参数中获取token
        if(StringUtils.isNullOrEmpty(token)){
            token = httpRequest.getParameter("token");
        }
        return "112233445566";//token;
    }
}

对于复杂的跨域请求,Vue会首先发送一个OPTIONS请求,进行验证。需要后端对所有接口统一处理放行OPTIONS方法(即返回200)即可

四:自定义Token

public class OAuth2Token implements AuthenticationToken {
	private String token;
	public OAuth2Token(String token) {
		this.token = token;
	}
	/*
	 * 登录提交的用户名
	 * 
	 */
	@Override
	public String getPrincipal() {
		return token;
	}
	/*
	 * 只被Subject 知道的秘密值,比如我们登录提供的密码
	 * 
	 */
	@Override
	public Object getCredentials() {
		return token;
	}
}

五:自定义OAuth2Realm

@Component
public class OAuth2Realm extends AuthorizingRealm {
	@Autowired
	private ShiroService shiroService;
	/*
	 * 判断此Realm是否支持此Token
	 */
	@Override
	public boolean supports(AuthenticationToken token) {
		return token instanceof OAuth2Token;
	}
	/*
	 * 提供用户信息返回权限信息 
	 * 
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		
		System.out.println("=============获取已登录用户,权限信息============");
		
		SysUserEntity user = (SysUserEntity) principals.getPrimaryPrincipal();
		Long userId = user.getUserId();
		// 用户权限列表
		Set<String> permsSet = shiroService.getUserPermissions(userId);
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		info.setStringPermissions(permsSet);
		return info;
	}
	/*
	 * 根据token获取认证信息
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		System.out.println("=============获取已登录用户,用户信息============");
		String accessToken = (String) token.getPrincipal();
		// 根据accessToken,查询用户信息
		SysUserTokenEntity tokenEntity = shiroService.queryByToken(accessToken);
		// token失效
		if (tokenEntity == null) {
			throw new IncorrectCredentialsException("token失效,请重新登录");
		}
		// 查询用户信息
		SysUserEntity user = shiroService.queryUser(tokenEntity.getUserId());
		// 账号锁定
		if (user.getStatus().equals("00")) {
			throw new LockedAccountException("账号已被锁定,请联系管理员");
		}
		SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName() // 返回一个唯一的Realm名字
		);
		return info;
	}
}

shiroService用来从数据库或者缓存中,查询用户信息和权限信息。

六:TokenGenerator

public class TokenGenerator {
    public static String generateValue() {
        return generateValue(UUID.randomUUID().toString());
    }
    private static final char[] hexCode = "0123456789abcdef".toCharArray();
    public static String toHexString(byte[] data) {
        if(data == null) {
            return null;
        }
        StringBuilder r = new StringBuilder(data.length*2);
        for ( byte b : data) {
            r.append(hexCode[(b >> 4) & 0xF]);
            r.append(hexCode[(b & 0xF)]);
        }
        return r.toString();
    }
    public static String generateValue(String param) {
        try {
            MessageDigest algorithm = MessageDigest.getInstance("MD5");
            algorithm.reset();
            algorithm.update(param.getBytes());
            byte[] messageDigest = algorithm.digest();
            return toHexString(messageDigest);
        } catch (Exception e) {
           e.printStackTrace();
        }
        return "";
    }
}

七:Controller层Demo

@RestController
@RequestMapping("shiro")
public class ShiroController {
	@RequestMapping(value = "/login", method = RequestMethod.GET)
	public String login() {
		return "login";
	}
	@RequestMapping(value = "/info", method = RequestMethod.GET)
	@RequiresPermissions("sys:config:info")
	public String info() {
		return "info";
	}
}

八:登录代码

	@PostMapping("/sys/login")
	public Map<String, Object> login(@RequestBody SysLoginForm form)throws IOException {
		boolean captcha = captchaService.validate(form.getUuid(), form.getCaptcha());
		if(!captcha){
			return R.error("验证码不正确");
		}
		//用户信息
		SysUserEntity user = sysUserService.queryByUserName(form.getUsername());
		//账号不存在、密码错误
		if(user == null || !user.getPassword().equals(new Sha256Hash(form.getPassword(), user.getSalt()).toHex())) {
			return R.error("账号或密码不正确");
		}
		//账号锁定
		if(Constant.CommonStatus.BANNED.getValue().equals(user.getStatus())){
			return R.error("账号已被锁定,请联系管理员");
		}
		//生成token,并保存到数据库
		R r = sysUserTokenService.createToken(user.getUserId());
		return r;
	}

登录验证,独立实现,验证成功后创建Token,Token放到Redis缓存中。前端页面请求接口时要带Token.

九:代码调用流程

1.登录成功后,创建Token,后面的接口调用都要传递Token

2.接口首先通过shiroFilter过滤,确定是否要进入OAuth2Filter进行处理

3. OAuth2Filter 首先执行isAccessAllowed方法,如果时OPTIONS请求直接放行,否则进行Shiro的executeLogin验证

4. executeLogin执行的时候会到OAuth2Realm中调用doGetAuthenticationInfo方法,根据Token获取当前登录用户的信息(Token与用户的关联,已在第八点自己实现的登录代码中实现)

5. executeLogin成功后,会看当前访问的接口,有无权限注解@RequiresPermissions

6.如果有注解的话,说需要进行权限验证,Shiro会通过 OAuth2Realm的doGetAuthorizationInfo方法,获取当前用户的权限进行验证

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/28624388/viewspace-2660254/,如需转载,请注明出处,否则将追究法律责任。

全部评论
10年以上互联网经验,先后从事过制造业,证券业,物业行业和物流行业信息系统和互联网产品的研发,6年系统架构经验。最近关注Kubernetes微服务架构和Istio微服务治理框架。

注册时间:2013-02-05

  • 博文量
    323
  • 访问量
    1098519