赞
踩
在web应用中有大量场景需要对用户进行安全校,一般人的做法就是硬编码的方式直接埋到到业务代码中,但可曾想过这样做法会导致代码不够简洁(大量重复代码)、有个性化时难维护(每个业务逻辑访问控制策略都不相同甚至差异很大)、容易发生安全泄露(有些业务可能不需要当前登录信息,但被访问的数据可能是敏感数据由于遗忘而没有受到保护)。为了更安全、更方便的进行访问安全控制,我们可以想到的就是使用springmvc的拦截器(HandlerInterceptor),但其实更推荐使用更为成熟的spring security来完成
认证和鉴权。
拦截器HandlerInterceptor确实可以帮我们完成登录拦截、或是权限校验、或是防重复提交等需求。其实基于它也可以实现基于url或方法级的安全控制。如果你对spring mvc的请求处理流程相对的了解,它的原理容易理解,具体可以参阅我之前的分享。
- public interface HandlerInterceptor {
- /**
- * Intercept the execution of a handler. Called after HandlerMapping determined
- * an appropriate handler object, but before HandlerAdapter invokes the handler.
- *
- * 在业务处理器处理请求之前被调用。预处理,可以进行编码、安全控制、权限校验等处理
- *
- * handler:controller内的方法,可以通过HandlerMethod method= ((HandlerMethod)handler);获取到@RequestMapping
- */
- boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
-
- /**
- * Intercept the execution of a handler. Called after HandlerAdapter actually
- * invoked the handler, but before the DispatcherServlet renders the view.
- *
- * 在业务处理器处理请求执行完成后,生成视图之前执行。后处理(调用了Service并返回ModelAndView,但未进行页面渲染),有机会修改ModelAndView
- */
- void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception;
-
- /**
- * Callback after completion of request processing, that is, after rendering
- * the view. Will be called on any outcome of handler execution, thus allows
- * for proper resource cleanup.
- *
- * 在DispatcherServlet完全处理完请求后被调用,可用于清理资源等。返回处理(已经渲染了页面)
- *
- */
- void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception;
- }
- //你可以基于有些url进行拦截
- @Configuration
- public class UserSecurityInterceptor extends WebMvcConfigurerAdapter {
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- String[] securityUrls = new String[]{"/**"};
- String[] excludeUrls = new String[]{"/**/esb/**", "/**/dictionary/**"};
- registry.addInterceptor(userLoginInterceptor()).excludePathPatterns(excludeUrls).addPathPatterns(securityUrls);
- super.addInterceptors(registry);
- }
-
- /** fixed: url中包含// 报错
- * org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL was not normalized.
- * @return
- */
- @Bean
- public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
- DefaultHttpFirewall firewall = new DefaultHttpFirewall();
- firewall.setAllowUrlEncodedSlash(true);
- return firewall;
- }
-
- @Bean
- public AuthInterceptor userLoginInterceptor() {
- return new AuthInterceptor();
- }
-
- public class AuthInterceptor implements HandlerInterceptor {
- public Logger logger = LoggerFactory.getLogger(AuthInterceptor.class);
- @Autowired
- private ApplicationContext applicationContext;
-
- public AuthInterceptor() {
- }
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- LoginUserInfo user = null;
- try {
- user = (LoginUserInfo) SSOUserUtils.getCurrentLoginUser();
- } catch (Exception e) {
- logger.error("从SSO登录信息中获取用户信息失败! 详细错误信息:%s", e);
- throw new ServletException("从SSO登录信息中获取用户信息失败!", e);
- }
-
- String[] profiles = applicationContext.getEnvironment().getActiveProfiles();
- if (!Arrays.isNullOrEmpty(profiles)) {
- if ("dev".equals(profiles[0])) {
- return true;
- }
- }
- if (user == null || UserUtils.ANONYMOUS_ROLE_ID.equals(user.getRoleId())) {
- throw new ServletException("获取登录用户信息失败!");
- }
- return true;
- }
-
- @Override
- public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
-
- }
-
- @Override
- public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
-
- }
- }
- }

确认一个访问请求发起的时候背后的用户是谁,他的用户信息是怎样的。在spring security 里面认证支持很多种方式,最简单的就是用户名密码,还有LDAP、OpenID、CAS等等。
而在我们的系统里面,用户信息需要通过kxtx-sso
模块进行获取。通过sso认证比较简单,就是要确认用户是否通过会员系统登录,并把登录信息包装成授权对象放到SecurityContext
中,通过一个filter来完成:
- @Data
- @EqualsAndHashCode(callSuper = false)
- public class SsoAuthentication extends AbstractAuthenticationToken {
- private static final long serialVersionUID = -1799455508626725119L;
-
- private LoginUserInfo user;
-
- public SsoAuthentication(LoginUserInfo user) {
- super(null);
- this.user = user;
- }
-
- @Override
- public Object getCredentials() {
- return "kxsso";
- }
-
- @Override
- public Object getPrincipal() {
- return user;
- }
-
- @Override
- public String getName() {
- return user.getName();
- }
- }
- public class SsoAuthenticationProcessingFilter extends OncePerRequestFilter {
- @Override
- protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
- throws ServletException, IOException {
- LoginUserInfo user = (LoginUserInfo) SSOUserUtils.getCurrentLoginUser();
- SsoAuthentication auth = new SsoAuthentication(user );
- SecurityContextHolder.getContext().setAuthentication(auth);
- filterChain.doFilter(request, response);
- }
- }
- @Component
- public class SsoAuthenticationProvider implements AuthenticationProvider {
-
- @Value("${env}")
- String env;
-
- @Override
- public Authentication authenticate(Authentication authentication) throws AuthenticationException {
- LoginUserInfo loginUserInfo = (LoginUserInfo) authentication.getPrincipal();
- /*
- * DEV环境允许匿名用户访问,方便调试,其他环境必须登录。
- */
- if (!UserUtils.ANONYMOUS_ROLE_ID.equals(loginUserInfo.getRoleId()) || "dev".equals(env)) {
- authentication.setAuthenticated(true);
- } else {
- throw new BadCredentialsException("请登录");
- }
- return authentication;
- }
-
- @Override
- public boolean supports(Class<?> authentication) {
- return SsoAuthentication.class.equals(authentication);
- }
- }
- @Configuration
- @EnableWebSecurity
- @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
- public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
-
- protected void configure(HttpSecurity http) throws Exception {
- // 关闭session
- http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and();
-
- // 允许访问所有URL,通过方法保护的形式来限制访问。
- http.authorizeRequests().anyRequest().permitAll();
-
- // 注册sso filter
- http.addFilterBefore(ssoAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
- }
-
- @Bean
- SsoAuthenticationProcessingFilter ssoAuthenticationProcessingFilter() {
- return new SsoAuthenticationProcessingFilter();
- }
- }

控制一个功能是否能被当前用户访问,对不符合要求的用户予以拒绝。spring security 主要有两种控制点:
而控制形式就比较多样化了:
目前鉴权的需求比较简单:登录允许访问,未登录禁止访问。因此可以定义了一个切面,控制所有需要安全控制的Controller。
spring security 提供了一些注解:
@PreAuthorize | 控制一个方法是否能够被调用,业务方法(HandlerMethod )的前置处理,比如: @PreAuthorize("#id<10")限制只能查询Id小于10的用户 @PreAuthorize("principal.username.equals(#username)")限制只能查询自己的信息 @PreAuthorize("#user.name.equals('abc')")限制只能新增用户名称为abc的用户 |
@PostAuthorize | 业务方法调用完之后进行权限检查,后置处理,比如: @PostAuthorize("returnObject.id%2==0") public User find(int id) {} 返回值的id是偶数则表示校验通过,否则表示校验失败,将抛出AccessDeniedException |
@PreFilter | 对集合类型的参数进行过滤,比如: 对集合ids中id不为偶数的进行移除 |
@PostFilter | 对集合类型的返回值进行过滤,比如: 将对返回结果中id不为偶数的list中的对象进行移除 @PostFilter("filterObject.id%2==0") |
@AuthenticationPrincipal | 解决在业务方法内对当前用户信息的方法 |
- @Aspect
- @Component
- public class InControllerAspect {
- @Autowired
- BeforeInControllerMethods beforeInMethods;
-
- @Pointcut("execution(public * com.kxtx.oms.portal.controller.in.*.*(..)) && @annotation(org.springframework.web.bind.annotation.RequestMapping)")
- public void hp() {
- };
-
- @Before("hp()")
- public void befor() {
- beforeInMethods.before();
- }
- }
- @Component
- public class BeforeInControllerMethods {
- //@PreAuthorize("authenticated")要求所有访问此方法的用户必须登录
- @PreAuthorize("authenticated")
- public void before() {
- }
- }
- //用户信息获取
- @RequestMapping("/order/submit")
- public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) {
- // .. find messages for this user and return them ...
- }

是不是有点复杂,复杂的是表现形式,实际上需要真正理解它的目的(为了要解决什么问题)。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。