springboot中使用security

最近在项目当中使用spring security做权限管理,下面简单记录一下如何配置和使用.

一、权限相关表

1、user:用户表

2、groups:用户组表

3、role:角色表

4、resource:资源表

5、user_group:用户、组关系表

6、group_role:用户组、角色关系表

7、role_resource:角色、资源关系表

一个用户可以在多个组中,一个组可以包括多个角色、一个角色可以包括多个资源。

这里的资源就是权限管理的目标,包括页面上的元素、后台方法等等。

二、pom

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>

spring-boot-starter-security是spring security核心包

thymeleaf-extras-springsecurity4是在thymeleaf中使用一些security标签的依赖包

三、在启动类中

因为springboot自动配置的原因,当加上Pom中的依赖后,系统自动就会加上一个默认的权限校验,给了一个随机的用户和密码,并且提供了一个登陆页面。在正常开发中,我们当然不会这么做,因此我们要关闭掉security的自动配置,如下:

@EnableAutoConfiguration(exclude = {SecurityAutoConfiguration.class})

四、配置spring security

1、

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@ConditionalOnProperty(name = "security.enabled")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private FilterSecurityInterceptor filterSecurityInterceptor;

    @Bean
    UserDetailsService customUserService() {
        //注册UserDetailsService 的bean
        return new CustomUserService();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //user Details Service验证,密码md5加密
        auth.userDetailsService(customUserService()).passwordEncoder(new PasswordEncoder() {
            @Override
            public String encode(CharSequence password) {
                return MD5Util.encode((String) password);
            }

            @Override
            public boolean matches(CharSequence password, String encodedPassword) {
                return encodedPassword.equals(MD5Util.encode((String) password));
            }
        });
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable() //关闭默认的跨域保护
                .authorizeRequests()
                //放开静态资源
                .antMatchers("/frame/**", "/i18n/**", "/images/**", "/css/**", "/js/**").permitAll()
                .anyRequest().authenticated() //任何请求,登录后可以访问
                .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .failureUrl("/login?error")
                .permitAll() //登录页面用户任意访问
                .and()
                .logout()
                .logoutSuccessUrl("/login")
                .permitAll(); //注销行为任意访问
        http.exceptionHandling().accessDeniedHandler(new SswAccessDeniedHandler());
        http.addFilterBefore(filterSecurityInterceptor, org.springframework.security.web.access.intercept.FilterSecurityInterceptor.class);
    }

}

说明一下:

@EnableWebSecurity 开启spring security

@EnableGlobalMethodSecurity(prePostEnabled = true) 在方法中可以使用@PreAuthorize("hasAuthority('admin')")等注解来控制某个方法的全年

FilterSecurityInterceptor自定义的拦截器,继承了AbstractSecurityInterceptor,security的功能基本上就是靠这个拦截器完成,后面会描述

CustomUserService自定义类,继承UserDetailsService,该类主要用来在登陆时加载用户信息及其权限信息给spring security.

http.exceptionHandling().accessDeniedHandler(new SswAccessDeniedHandler());自定义了当没有权限访问时的处理策略。

其他的在注释中都有描述,就不细说了

2、


@Component("customUserService")
@ConditionalOnProperty(name = "security.enabled")
public class CustomUserService implements UserDetailsService {

    @Resource
    private UserDao userDao;
    @Resource
    private ResourceDao resourceDao;

    @Override
    public UserDetails loadUserByUsername(String loginName) throws UsernameNotFoundException {
        User user = userDao.findByLoginName(loginName);
        if (user == null) {
            throw new UsernameNotFoundException("用户: " + loginName + " 不存在!");
        }
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        List<com.aas.ssw.business.example.entity.Resource> resourceList = resourceDao.findByUserId(user.getId());
        if(resourceList == null || resourceList.size() == 0){
            return new org.springframework.security.core.userdetails.User(user.getLoginName(), user.getPassword(), grantedAuthorities);
        }
        for (com.aas.ssw.business.example.entity.Resource resource : resourceList) {
            if (resource != null && resource.getName() != null) {
                GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(resource.getName());
                //1:此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。
                grantedAuthorities.add(grantedAuthority);
            }
        }
        return new org.springframework.security.core.userdetails.User(user.getLoginName(), user.getPassword(), grantedAuthorities);
    }
}

当有用户登陆时,spring security会调用loadUserByUsername方法,并把用户输入的账号传进来,但是并不传密码,因为这个方法不会做用户名和密码的校验,该方法只是根据用户名从数据库中查出来用户的信息,然后将其交给spring security来根据这个信息和用户输入的账号密码来校验登陆是否成功。如果登陆成功,那么spring security会将用户信息、权限等保存在内存中,以便后边使用。也就是说,登陆校验是由spring security来做的,不需要我们显式的处理。

3、

@Component("filterInvocationSecurityMetadataSource")
@ConditionalOnProperty(name = "security.enabled")
public class InvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
    @Resource
    private ResourceDao resourceDao;
    private ConcurrentHashMap<String, List<ConfigAttribute>> map = new ConcurrentHashMap<>();

    /**
     * 加载资源表中所有资源
     */
    @PostConstruct
    private void loadAllResource() {
        List<com.aas.ssw.business.example.entity.Resource> resourceList = resourceDao.findAll();
        for (com.aas.ssw.business.example.entity.Resource resource : resourceList) {
            ConfigAttribute securityConfig = new SecurityConfig(resource.getName());
            List<ConfigAttribute> configAttributeList = map.get(resource.getUrl());
            if(configAttributeList == null || configAttributeList.size() == 0){
                configAttributeList = new ArrayList<>();
                configAttributeList.add(securityConfig);
                map.put(resource.getUrl(), configAttributeList);
            }else {
                configAttributeList.add(securityConfig);
            }
        }
    }

    /**
     * 此方法是为了判定用户请求的url是否在资源表中,
     * 如果在资源表中,则返回给 decide 方法,用来判定用户是否有此权限。
     * 如果不在权限表中则放行。
     * @param o
     * @return
     * @throws IllegalArgumentException
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        //object 中包含用户请求的request 信息
        HttpServletRequest request = ((FilterInvocation) o).getHttpRequest();
        for (String resourceUrl : map.keySet()) {
            AntPathRequestMatcher matcher = new AntPathRequestMatcher(resourceUrl);
            if(matcher.matches(request)) {
                return map.get(resourceUrl);
            }
        }
        return null;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

该类有两个作用,第一个是在系统初始化的时候加载所有需要校验的资源。也就是说在这里被加载的资源在访问时会被spring security校验。第二个时每当有访问请求时,会判断请求的资源是不是需要校验,如果时则交给AccessDecisionManager来做判断,如果不是则直接放行。

4、

@Component("accessDecisionManager")
@ConditionalOnProperty(name = "security.enabled")
public class SswAccessDecisionManager implements AccessDecisionManager {


    /**
     * decide 方法是判定是否拥有权限的决策方法,
     * authentication 是用户拥有的所有资源.
     * object 包含客户端发起的请求的requset信息;
     * configAttributes 为InvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果 即需要检验的资源
     *
     * @param authentication
     * @param object
     * @param configAttributes
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        if (null == configAttributes || configAttributes.size() <= 0) {
            return;
        }
        for (ConfigAttribute configAttribute : configAttributes) {
            String needRole = configAttribute.getAttribute();
            for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                //authentication 为在注释1 中循环添加到 GrantedAuthority 对象中的权限信息集合
                if (needRole.trim().equals(grantedAuthority.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("没有权限");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

该类的主要作用就是校验用户是否拥有要访问的资源的权限,如果有则可以访问,否则则抛出异常,该异常会被AccessDeniedHandler捕获处理。

5、

/**
 * 自定义security没有权限时的处理策略
 */
public class SswAccessDeniedHandler implements AccessDeniedHandler {
    private final String errorPage = "/accessDeny";

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        boolean isAjax = RequestUtil.isAjaxRequest(request);
        //ajax请求返回json数据,非ajax请求跳转至自定义页面
        if (isAjax) {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            PrintWriter out = null;
            try {
                out = response.getWriter();
                out.append(JSONObject.toJSONString(Result.getResult(Constant.FAIL,"没有权限",null,null)));
                out.flush();
            } finally {
                if(out != null){
                    out.close();
                }
            }
        } else if (!response.isCommitted()) {
            if (errorPage != null) {
                // Put exception into request scope (perhaps of use to a view)
                request.setAttribute(WebAttributes.ACCESS_DENIED_403, e);
                // Set the 403 status code.
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                // forward to error page.
                RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
                dispatcher.forward(request, response);
            } else {
                response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
            }
        }
    }
}

该类用来处理没有权限时的应该怎么做,这里主要时区分了一下是否是ajax请求,如果时则返回一个json,不是则跳转到自定义的页面中。

6、

@Component("filterSecurityInterceptor")
@ConditionalOnProperty(name = "security.enabled")
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
    @Resource
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    @Resource
    public void setMyAccessDecisionManager(SswAccessDecisionManager accessDecisionManager) {
        super.setAccessDecisionManager(accessDecisionManager);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        FilterInvocation filterInvocation = new FilterInvocation(request, response, filterChain);
        invoke(filterInvocation);
    }

    private void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
        //filterInvocation里面有一个被拦截的url
        //里面调用InvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取filterInvocation对应的所有权限
        //再调用SswAccessDecisionManager的decide方法来校验用户的权限是否足够
        InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
        try {
        //执行下一个拦截器
            filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }

    @Override
    public void destroy() {

    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.securityMetadataSource;
    }
}

主要靠该拦截器完成权限校验,具体来说,该拦截器使用前边定义的AccessDecisionManager,InvocationSecurityMetadataSourceService完成校验工作。

五、使用

主要有两种使用场景

1、页面当中的元素:

<div sec:authorize="hasAuthority('admin')">管理员才能看见</div>
<div sec:authorize="hasAuthority('user')">管理员和普通用户都能看见</div>

2、某个具体的方法:

@GetMapping("/test3")
@ResponseBody
@PreAuthorize("hasAuthority('admin')")
public String test3(){
return "hehe";
}
都很简单,就不解释了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值