springboot-security笔记

springboot-security笔记

开始

  • 基于springboot笔记搭建项目;
  • 依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • 编写securityConfig权限配置类继承WebSecurityConfigurerAdapter,下面步骤中配置都在这里面做配置;

用户和密码

用户信息类

public class LoginUser extends User {

    @Getter
    @Setter
    private String detail;

    public LoginUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }

    public LoginUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
    }
}

获取用户类

编写获取用户的Bean:UserDetailsService

@Autowired
private PasswordEncoder passwordEncoder;
@Bean
public UserDetailsService userDetailsService() {
    // todo: 从数据库查询用户和角色信息
    // 方式一
    UserDetailsService userDetailsService = new UserDetailsService() {
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            LoginUser user = new LoginUser(username, passwordEncoder.encode("1234"),
                                           AuthorityUtils.commaSeparatedStringToAuthorityList(username));
            return user;
        }
    };
    // 方式二
    /*JdbcDaoImpl userDetailsService = new JdbcDaoImpl(){
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //自定义查询用户
            return super.loadUserByUsername(username);
        }
    };
    // 方式三:自定义查询用户与角色的sql
    userDetailsService.setDataSource(dataSource);
    // select需要有3个字段:username、password、enable
    userDetailsService.setUsersByUsernameQuery("select * from user where username=?");
    userDetailsService.setAuthoritiesByUsernameQuery("select username, role from user u, role r, user_role ur where u.id=ur.uid and r.id=ur.rid and uid=?");
    */
    return userDetailsService;
}

密码编码器

@Bean
public PasswordEncoder passwordEncoder() {
    // 使用spring security自带的
    // return new BCryptPasswordEncoder();
    // 使用自定义的
    // todo: 密码加密加盐
    return new PasswordEncoder() {
        @Override
        public String encode(CharSequence rawPassword) {
            return rawPassword.toString();
        }

        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            System.out.println("===>用户输入密码:" + rawPassword);
            System.out.println("===>数据库密码:" + encodedPassword);
            return rawPassword.equals(encodedPassword);
        }
    };
}

配置

配置用户查询方法与密码匹配器

@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}

忽略的地址

说明:一般用来配置无需权限校验的路径,也可以在HttpSecurity中配置,但是在web.ignoring()中配置效率更高。web.ignoring()是一个忽略的过滤器,而HttpSecurity中定义了一个过滤器链,即使permitAll()放行还是会走所有的过滤器,直到最后一个过滤器FilterSecurityInterceptor认定是可以放行的,才能访问。配置如下:

@Override
public void configure(WebSecurity web) {
    web.ignoring().antMatchers("/static/**");
}

登录失败处理

@Bean
public AuthenticationFailureHandler failureHandler() {
    return new AuthenticationFailureHandler() {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
            if (e instanceof UsernameNotFoundException) {
                System.out.println(e.getMessage());
                request.getRequestDispatcher("/loginPage?msg=用户不存在").forward(request, response);
            } else if (e instanceof BadCredentialsException) {
                System.out.println(e.getMessage());
                request.getRequestDispatcher("/loginPage?msg=用户名或密码错误").forward(request, response);
            } else {
                System.out.println(e.getMessage());
                request.getRequestDispatcher("/loginPage?msg=账号异常").forward(request, response);
            }
        }
    };
}

记住密码

用于将记住密码的信息保存到数据库,不使用该类默认会保存在cookie中

@Bean
public PersistentTokenRepository persistentTokenRepository(){
    JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
    tokenRepository.setDataSource(dataSource);
    //系统在启动的时候生成“记住我”的数据表(只能使用一次)
    //tokenRepository.setCreateTableOnStartup(true);
    return tokenRepository;
}

自定义资源权限

springsecurity默认只支持配置式的角色资源权限,使用不够灵活,此部分就是用于自定义角色资源权限的,如不需要,可以不配置。

权限数据源

  • 新建资源权限数据源类,用于加载资源权限;
@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    /**
     * 资源权限, key为url,value为可以访问该url的角色列表
     */
    private volatile LinkedHashMap<String, Collection<ConfigAttribute>> urlPermMap = new LinkedHashMap<>();

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    public void setUrlPermMap(){
        // todo:从数据库获取
        Collection<ConfigAttribute> perm1 = new HashSet<>();
        perm1.add(() -> "admin");
        urlPermMap.put("/admin", perm1);
        Collection<ConfigAttribute> perm2 = new HashSet<>();
        perm2.add(() -> "user");
        urlPermMap.put("/user", perm2);
        Collection<ConfigAttribute> perm3 = new HashSet<>();
        perm3.add(() -> "user");
        perm3.add(() -> "admin");
        urlPermMap.put("/info", perm3);
    }

    public MySecurityMetadataSource(){
        // 初始化资源权限
        setUrlPermMap();
    }

    // 凡是被springSecurity拦截的请求都会执行该方法
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation) o;
        String url = fi.getRequestUrl();
        // 资源权限为空,初始化资源
        for (String urlPerm : urlPermMap.keySet()) {
            if(antPathMatcher.match(urlPerm, url)){
                // 根据url返回角色集合
                return urlPermMap.get(urlPerm);
            }
        }
        // 这个url没有配权限
        return null;
    }

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

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

决策管理器

自定义决策管理器,判断是否有访问权限;

@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        // 没有角色集合时,放行
        if (collection == null || collection.isEmpty()) {
            return;
        }
        // 当前登录的用户包含当前url的角色时放行
        for (GrantedAuthority currRole : authentication.getAuthorities()) {
            for (ConfigAttribute urlRole : collection) {
                if(currRole.getAuthority().equals(urlRole.getAttribute())){
                    // 拥有权限,放行
                    return;
                }
            }
        }
        throw new AccessDeniedException("没有权限");
    }

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

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

http配置

配置HttpSecurity

@Autowired
private MyAccessDecisionManager myAccessDecisionManager;
@Autowired
private MySecurityMetadataSource mySecurityMetadataSource;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationFailureHandler failureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers("/myLogin", "/loginNoPass").permitAll()
        .anyRequest().authenticated()
        .and().formLogin().loginPage("/loginPage").loginProcessingUrl("/login")
        .defaultSuccessUrl("/").failureHandler(failureHandler)//.failureForwardUrl("/loginPage")
        .permitAll()
        .usernameParameter("user").passwordParameter("pass")
        .and().logout().logoutSuccessUrl("/loginPage")//.logoutSuccessHandler(null)
        .and().exceptionHandling().accessDeniedPage("/deny")//.accessDeniedHandler(null)
        // 使用userDetailsService用Token从数据库中获取用户自动登录
        .and().rememberMe().tokenValiditySeconds(1800).key("token_key").userDetailsService(userDetailsService)
        .and().csrf().disable();
    // .exceptionHandling().authenticationEntryPoint((req, res, auth) -> {res.sendRedirect("/loginPage");});

    // 使用自定义url权限,则上面配置的url权限会失效
    FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
    filterSecurityInterceptor.setSecurityMetadataSource(mySecurityMetadataSource);
    filterSecurityInterceptor.setAccessDecisionManager(myAccessDecisionManager);
    http.addFilterBefore(filterSecurityInterceptor, FilterSecurityInterceptor.class);
}

上面的配置依次为

  1. 无需授权就可以访问的url
  2. 其他所有url都需要授权验证
  3. 登录页url,登录url
  4. 登录成功/失败时的跳转地址(处理方法)
  5. 登录的用户名、密码字段
  6. 退出成功的跳转地址(处理方法)
  7. 没有权限时的跳转地址(处理方法)
  8. 记住密码功能(默认保存在cookie,可以通过tokenRepository配置为保存在数据库----这对表有固定格式要求)
  9. 关闭csrf
  10. 未登录访问的处理方法,配置了该项,则第3项的登录页url跳转功能将失效;
  11. 最后4行的配置用于自定义url资源权限,即动态实现角色和url的拦截,如不需要,可以删除;

附:代码中注释掉的处理方法,主要是针对前后端分离的项目,在前后端分离的项目中,需要将跳转地址改为自定义处理方法,用于返回权限json。

到此,登录已经可以使用了,其中/loginPage为自定义的登录页面,/login为登录接口,接收userpassremember-me三个参数。

自定义登录

配置

要使用自定义登录,需要先有一个认证管理器;

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

自定义登录

除了访问内置的登录接口进行登录,也可以使用下面的方法进行登录:

@Autowired
private AuthenticationManager authenticationManager;
@RequestMapping(value = "/myLogin")
public String login(String user, String pass, HttpServletRequest request, HttpServletResponse response){
    System.out.println(request.getMethod());
    if(user == null || pass == null){
        return "login";
    }
    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, pass);
    try{
        //使用SpringSecurity拦截登陆请求 进行认证和授权
        Authentication authenticate = authenticationManager.authenticate(token);
        SecurityContextHolder.getContext().setAuthentication(authenticate);
        request.getSession().setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
        //记住密码功能(还需要在请求中添加参数:remember-me=true)
        RememberMeServices rememberMeServices = new TokenBasedRememberMeServices("token_key", userDetailsService);
        rememberMeServices.loginSuccess(request, response, authenticate);
    }catch (Exception e){
        e.printStackTrace();
        return "login";
    }
    return "redirect:/";
}

免密登录

某些情况下,我们可能不需要账号密码进行登录和授权,这个方法就可以做到;

@RequestMapping("/loginNoPass")
public String loginNoPass(String username, HttpServletRequest request){
    //Arrays.asList(SimpleGrantedAuthority())
    User user = new User(username, "1", AuthorityUtils.createAuthorityList("admin"));
    // 使用这个token,则不会在进行认证和授权,需要在这里完成授权
    PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(user, user.getPassword(), user.getAuthorities());
    token.setDetails(new WebAuthenticationDetails(request));
    SecurityContextHolder.getContext().setAuthentication(token);
    request.getSession().setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());
    return "redirect:/";
}

用户信息

用户信息一般可以在SecurityContextHolder中获取

@RequestMapping("/info")
public String info(Model model, HttpServletRequest request){
    // request.getUserPrincipal()与SecurityContextHolder.getContext()返回值相同
    // 第一行能获取到LoginUser, (这里强转为UsernamePasswordAuthenticationToken的父类,避免不同登录方式会报错的问题)
    model.addAttribute("data", "loginUser: ===>" + ((AbstractAuthenticationToken) request.getUserPrincipal()).getPrincipal() +
                       "\nremoteUser: ===>" + request.getRemoteUser() +
                       "\ncontext: ===>" + SecurityContextHolder.getContext().getAuthentication().getName());
    return "index";
}

redis

springsecurity使用的session来管理认证信息,要整合redis只需要使用redis session即可,步骤如下:

  • 依赖
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
  • 配置
# 使用redis session
spring.session.store-type=redis
  • 启动类添加注解@EnableRedisHttpSession

此时,不仅仅springsecurity使用了redis,就是我们代码中使用的普通session也会存入redis,此时,就可以通过nginx进行负载均衡了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值