Security授权

一个最简单的security示例代码

1.1权限认证流程

SpringSecurity是基于Filter实现认证和授权,底层通过FilterChainProxy代理去调用各种Filter(Filter链),Filter通过调用AuthenticationManager完成认证 ,通过调用AccessDecisionManager完成授权。流程如下图

springsecurity 权限存入token 膨胀 springsecurity权限控制流程_java

SecurityContextPersistenceFilter
———SecurityContext :存储认证用户的上下文对象
———SecurityContextRepository:用来存储SecurityContext 的仓库
———SecurityContextHolder:用来操作SecurityContext 的工具
UsernamePasswordAuthenticationFilter
———默认拦截“/login”登录请求,处理表单提交的登录认证,将请求中的认证信息包括username,password等封装成UsernamePasswordAuthenticationToken,然后调用AuthenticationManager的认证方法进行认证。
BasicAuthenticationFilter
———基本认证,httpBasic登录,弹出登录框登录
RememberAuthenticationFilter
———匿名Filter,用来处理匿名访问的资源,如果SecurityContext中没有Authentication,就会创建匿名的Token(AnonymousAuthenticationToken),然后通过SecurityContextHodler设置到SecurityContext中。
FilterSecurityInterceptor
———用来做授权的Filter,通过父类(AbstractSecurityInterceptor.beforeInvocation)调用AccessDecisionManager.decide方法对用户进行授权。

1.2Security相关概念

Authentication
所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,比如 最容易理解的UsernamePasswordAuthenticationToken,其中包含了用户名和密码。其实就是User对象

AuthenticationManager
用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给 AuthenticationManager的authenticate()方法来实现认证。AuthenticationManager会调用AuthenticationProvider.authenticate进行认证。认证成功后,返回一个包含了认 证信息的Authentication对象。

AuthenticationProvider.authenticate
认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我 是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider;如果我是通 过CAS请求单点登录系统实现,那就有一个CASProvider。按照Spring一贯的作风, 主流的认证方式它都已经提供了默认实现,比如DAO、LDAP、CAS、OAuth2等。 前 面讲了AuthenticationManager只是一个代理接口,真正的认证就是由 AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider, 每个provider通过实现一个support方法来表示自己支持那种Token的认证。 AuthenticationManager默认的实现类是ProviderManager。

UserDetailService
用户的认证通过Provider来完成,而Provider会通过UserDetailService拿到数据库(或 内存)中的认证信息然后和客户端提交的认证信息做校验。虽然叫Service,但是我更愿 意把它认为是我们系统里经常有的UserDao。

SecurityContext
当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用 户的认证信息Authentication。通过SecurityContext我们可以获取到用户的标识 Principle和授权信息GrantedAuthrity。在系统的任何地方只要通过 SecurityHolder.getSecruityContext()就可以获取到SecurityContext。在Shiro中通过 SecurityUtils.getSubject()到达同样的目的

1.3.SpringSecurity认证流程原理

springsecurity 权限存入token 膨胀 springsecurity权限控制流程_ide_02


springsecurity 权限存入token 膨胀 springsecurity权限控制流程_ide_03


认证原理:

1.请求先到一个持久SecurityContext的filter: 从SecurityContextRepository仓库中存,取securitycontxt到securityContextHolder中

2.请求到一个UsernamepasswordAuthenticationFilter:封装表单中的用户名密码成UsernamepasswordAuthenticationToken对象,调用authticationManger做认证.

3.authticationManger调用authenticationProvider做认证

4.authenticationProvider调用userDetailsServer加载数据库中的用户信息(UserDeatils) [对应的是我们简单示例代码配置文件中UserDetailsService方法 ]

5.AuthenticationProvider会调用PasswordEncoder对比传入的密码和数据库查询的密码

6.校验成功,把用户信息又封装成一个UsernamepasswordAuthenticationToken对象,设置给SecurityContext

7.把SecurityContext设置到SecurityContextHolder中

8.持久SecurityContext的filter会从SecurityContextHolder中取出SecurityContext设置到securityContextRepository中存储起来,删除holder中的数据

9.接下来访问资源的时候持久SecurityContext的filter会从securityContextRepository中取出securityContext设置到SecurityContextHolder,以后在代码中直接通过SecurityContextHolder就可以获取认证信息了

2.0定义一个认证流程

在SpringSecurity的整个认证流程中,除了UserDetailsService需要我们自己定义外,其他的的组件都可以使用默认的,因为UserDetailsService是SpringSecurity获取数据库中的认证信息的媒介,而如何才能从数据库中获取认证信息只有我们才知道。在入门案例中我们使用的是InMemoryUserDetailsManager 基于内存的UserDetailsService方案,接下来我们需要把基于内存的方案修改为基于数据库的方案。

2.1首先我们需要定义一个UserDetailsService

1.我们去实现UserDetailsService 这个类覆写loadUserByUsername
2.我们去数据库将用户及用户权限信息查询到并封装到我们的UserDetails中(也就是需要返回的类)但是因为是interface修饰的所以我们用其子类User类进行封装
这是需要传入的数据 GrantedAuthority 用来封装权限信息

package cn.zhangqiang.userdetials;

import cn.zhangqiang.domain.Permission;
import cn.zhangqiang.domain.VipUser;
import cn.zhangqiang.mapper.PermissionMapper;
import cn.zhangqiang.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.stream.Collectors;
@Component
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private PermissionMapper permissionMapper;


    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        
        //查询到当前用户 将当前用户的信息封装成一个UserDetails对象进行返回
        VipUser user = userMapper.selectByUsername(s);
        //查询到所有权限
        List<Permission> permissions = permissionMapper.selectPermissionsByUserId(user.getId());
        //将Permission对象转换为SimpleGrantedAuthority对象
        //new SimpleGrantedAuthority();

        List<SimpleGrantedAuthority> collect = permissions.stream().map(permission -> {
            return new SimpleGrantedAuthority(permission.getExpression());
        }).collect(Collectors.toList());


        //String username, String password, boolean enabled, boolean accountNonExpired,
        // boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities
        UserDetails userDetails = new User(user.getUsername(),user.getPassword(),user.isEnabled(),user.isAccount_non_expired(),
                user.isCredentials_non_expired(),user.isAccount_non_locked(),collect);

        return userDetails;
    }
}

2.2Yml配置

spring:
  datasource:
    url: jdbc:mysql:///hrm-security
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
mybatis:
  mapper-locations: classpath:cn/zhangqiang/mapper/*Mapper.xml

2.3认证成功处理

自定义类实现AuthenticationSuccessHandler接口复写 onAuthenticationSuccess方法,该方法其中一个参数是Authentication ,他里面封装了认证信息,用户信息UserDetails等,我们需要在这个方法中使用Response写出json数据即可

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) 
throws IOException, ServletException {
        Map map = new HashMap<>();
        map.put("success",true);
        map.put("message","认证成功");
        response.getWriter().print(JSON.toJSONString(map));
        response.getWriter().flush();
        response.getWriter().close();
    }
}

需要导入一个依赖

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.50</version>
</dependency>

2.4认证失败处理

自定义登录失败的处理,需要实现AuthenticationFailureHandler接口,复写onAuthenticationFailure方法实现自己的认证失败结果处理

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        Map map = new HashMap<>();
        map.put("success",false);
        map.put("message","认证失败");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().print(JSON.toJSONString(map));
        response.getWriter().flush();
        response.getWriter().close();
    }
}

2.5授权失败处理

package cn.zhangqiang.handel;

import com.alibaba.fastjson.JSON;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class DefaultAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        Map map = new HashMap<>();
        map.put("success",false);
        map.put("message","没有权限--默认访问被拒绝处理程序:"+accessDeniedException.getMessage());
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.getWriter().print(JSON.toJSONString(map));
        response.getWriter().flush();
        response.getWriter().close();
    }
}

2.6定义身份入口点

package cn.zhangqiang.handel;

import com.alibaba.fastjson.JSON;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response,
                         AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        Map map = new HashMap<>();
        map.put("success",false);
        map.put("message","没有权限--我的身份验证入口点:"+e.getMessage());
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.getWriter().print(JSON.toJSONString(map));
        response.getWriter().flush();
        response.getWriter().close();
    }
}

2.7对config文件进行配置

package cn.zhangqiang.config;

import cn.zhangqiang.domain.Permission;
import cn.zhangqiang.handel.DefaultAccessDeniedHandler;
import cn.zhangqiang.handel.MyAuthenticationEntryPoint;
import cn.zhangqiang.handel.MyAuthenticationFailureHandler;
import cn.zhangqiang.handel.MyAuthenticationSuccessHandler;
import cn.zhangqiang.mapper.PermissionMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.List;

@SuppressWarnings("ALL")
@Configuration
@EnableWebSecurity
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//因为基于数据库所以这个就不要了
//     //提供用户信息,这里没有从数据库查询用户信息,在内存中模拟
//    @Bean
//    public UserDetailsService userDetailsService(){
//
//        InMemoryUserDetailsManager inMemoryUserDetailsManager =
//        new InMemoryUserDetailsManager();
//        inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("123").authorities("admin").build());
//
//
//
//        return inMemoryUserDetailsManager;
//    }

    @Autowired
    private DefaultAccessDeniedHandler defaultAccessDeniedHandler;
    @Autowired
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private PermissionMapper permissionMapper;

    //密码编码器:不加密
    @Bean
    public PasswordEncoder passwordEncoder(){
        //return NoOpPasswordEncoder.getInstance();
        //切换一个加密方式
        return new BCryptPasswordEncoder();
    }
    
    //授权规则配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //这是循环进行授权  (这些数据都是来自于数据库)
        List<Permission> permissions = permissionMapper.selectAll();
        permissions.forEach(permission -> {
            try {
                http.authorizeRequests().antMatchers(permission.getResource()).hasAuthority(permission.getExpression());
                log.info("为资源授权{} -> {}",permission.getResource(),permission.getExpression());
            } catch (Exception e) {
                e.printStackTrace();
            }
            //http.authorizeRequests().antMatchers(permission.getResource()).hasAuthority(permission.getExpression());
        });

        //授权失败处理
        http.exceptionHandling()
                .authenticationEntryPoint(myAuthenticationEntryPoint)
                .accessDeniedHandler(defaultAccessDeniedHandler);

        http.authorizeRequests()                               //授权配置
                .antMatchers("/login","/login.html").permitAll()  //登录路径放行
                .anyRequest().authenticated()                   //其他路径都要认证之后才能访问

                .and().formLogin()                              //允许表单登录
                .successHandler(myAuthenticationSuccessHandler) //认证成功结果处理
                .failureHandler(myAuthenticationFailureHandler) //认证结果失败处理
                .successForwardUrl("/loginSuccess")             // 设置登陆成功页
                .loginPage("/login.html")   //登录页面地址
                .loginProcessingUrl("/login")   //登录提交地址
                .and().logout().permitAll()                    //登出路径放行 /logout
                .and().csrf().disable();                        //关闭跨域伪造检查

    }
}

扩展–security的记住我功能实现

springsecurity 权限存入token 膨胀 springsecurity权限控制流程_java_04

需要自定义的登录页中name=“remember-me” 这是约定

<div class="checkbox">
<label><input type="checkbox" id="rememberme" name="remember-me"/>记住我</label>
</div>

配置文件WebSecurityConfig修改

package cn.zhangqiang.config;

import cn.zhangqiang.domain.Permission;
import cn.zhangqiang.handel.DefaultAccessDeniedHandler;
import cn.zhangqiang.handel.MyAuthenticationEntryPoint;
import cn.zhangqiang.handel.MyAuthenticationFailureHandler;
import cn.zhangqiang.handel.MyAuthenticationSuccessHandler;
import cn.zhangqiang.mapper.PermissionMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;
import java.util.List;

@SuppressWarnings("ALL")
@Configuration
@EnableWebSecurity
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

//     //提供用户信息,这里没有从数据库查询用户信息,在内存中模拟
//    @Bean
//    public UserDetailsService userDetailsService(){
//
//        InMemoryUserDetailsManager inMemoryUserDetailsManager =
//        new InMemoryUserDetailsManager();
//        inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("123").authorities("admin").build());
//
//
//
//        return inMemoryUserDetailsManager;
//    }

    @Autowired
    private DefaultAccessDeniedHandler defaultAccessDeniedHandler;
    @Autowired
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private PermissionMapper permissionMapper;

    //记住我需要
    @Autowired
    private DataSource  dataSource;
    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl obj = new JdbcTokenRepositoryImpl();
        obj.setDataSource(dataSource);
        //obj.setCreateTableOnStartup(true);
        //启动创建表persistent_logs表,存token,username时会用到
        //数据库中没有表的时候启用一下如果有表第二次启动会报错,所以启动以后会注释掉
        return obj;
    }


    //密码编码器:不加密
    @Bean
    public PasswordEncoder passwordEncoder(){
        //return NoOpPasswordEncoder.getInstance();
        //切换一个加密方式
        return new BCryptPasswordEncoder();
    }
    
    //授权规则配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //这是循环进行授权  (这些数据都是来自于数据库)
        List<Permission> permissions = permissionMapper.selectAll();
        permissions.forEach(permission -> {
            try {
                http.authorizeRequests().antMatchers(permission.getResource()).hasAuthority(permission.getExpression());
                log.info("为资源授权{} -> {}",permission.getResource(),permission.getExpression());
            } catch (Exception e) {
                e.printStackTrace();
            }
            //http.authorizeRequests().antMatchers(permission.getResource()).hasAuthority(permission.getExpression());
        });

        //授权失败处理
        http.exceptionHandling()
                .authenticationEntryPoint(myAuthenticationEntryPoint)          //-----------------------------------------------------
                .accessDeniedHandler(defaultAccessDeniedHandler);                //-----------------------------------------------------


        //记住我
        http.rememberMe()
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(360000)
                .userDetailsService(userDetailsService);



        http.authorizeRequests()                               //授权配置
                .antMatchers("/login","/login.html").permitAll()  //登录路径放行
                .anyRequest().authenticated()                   //其他路径都要认证之后才能访问

                .and().formLogin()                              //允许表单登录
                .successHandler(myAuthenticationSuccessHandler) //认证成功结果处理 ------------------------------------------------------
                .failureHandler(myAuthenticationFailureHandler) //认证结果失败处理 -------------------------------------------------------
                .successForwardUrl("/loginSuccess")             // 设置登陆成功页
                .loginPage("/login.html")   //登录页面地址
                .loginProcessingUrl("/login")   //登录提交地址
                .and().logout().permitAll()                    //登出路径放行 /logout
                .and().csrf().disable();                        //关闭跨域伪造检查

    }
}

OK完成 点击记住我 退出后依然能直接访问到自己的个人页面不需要登录

有需要的可以pl找我拿数据库的sql,和整个测试demo