【Spring Boot系列学习】17.权限管理框架

一、

 

 

二、Spring Security

Spring Security的认证流程可参考https://blog.csdn.net/u013435893/article/details/79605239

1.基本环境搭建

1.1导入依赖

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

1.2.建立一个web层请求接口(非必须)

@RestController
@RequestMapping("/user")
public class UserController {
    @GetMapping
    public String getUsers() {       
        return "Hello Spring Security";
    }
}

1.3运行并访问请求接口

我们知道,在未添加Spring Security前,在浏览器访问http://localhost:8080/user或http://localhost:8080,会在页面中显示Hello Spring Security;添加该权限框架的依赖后,此时再次访问,无法正常访问,弹出了身份验证(登录)页面,

  其原因是在Springboot中,Spring Security默认是生效的,当添加了该依赖后,所有的接口(访问路径)都是受保护的,需要验证才能正常访问,Spring Security提供了一个默认的用户,用户名是user,而密码则是启动项目的时候自动生成的。也可通过配置文件修改默认的用户名和密码。查看项目启动的日志,会发现如下的一段Log

直接用user和启动日志中的密码登录即可正常调用接口了。

其中的登录界面是Spring Security框架提供的默认页面,被称为HttpBasicLogin。

2.自定义用户认证逻辑

在实际生产中,不会使用默认的认证逻辑和登录页面,需根据实际项目来定义其逻辑和页面。

2.1.配置用户认证逻辑

Spring Security默认的认证逻辑是UserDetailsService,在实际生产项目中,我们需要有自己的认证逻辑,则可实现该接口。

@Service("userDetailsService")
public class CustomUserDetailService implements UserDetailsService {
        @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库中取出用户信息
        // 判断用户是否存在
        // 封装用户信息,并返回。参数分别是:用户名,密码,用户权限
        return new User("zhangsan", new BCryptPasswordEncoder(4).encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

2.1.1UserDetails

在上面认证逻辑中返回的对象就是UserDetails,该类封装了用户信息的对象,里面包含了七个方法,在返回该对象的时候,也可通过调用其实现类的构造方法设置值。

public interface UserDetails extends Serializable {
    // 封装了权限信息
    Collection<? extends GrantedAuthority> getAuthorities();
    // 密码信息
    String getPassword();
    // 登录用户名
    String getUsername();
    // 帐户是否过期
    boolean isAccountNonExpired();
    // 帐户是否被冻结
    boolean isAccountNonLocked();
    // 帐户密码是否过期,一般有的密码要求性高的系统会使用到,比较每隔一段时间就要求用户重置密码
    boolean isCredentialsNonExpired();
    // 帐号是否可用
    boolean isEnabled();
}

2.1.2密码加密解密

SpringSecurity中有一个PasswordEncoder接口,在实际生产中,我们只需实现该接口,并配置到容器中即可。默认为我们提供了一个实现类BCryptPasswordEncoder,我们只需将其注入到容器中。

2.2.Spring Security相关配置

创建配置类

@Configuration    //指明该类是一个配置类
@EnableWebSecurity  //开启 Security 服务
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启全局 Securtiy 注解,开启方法权限控制
@Slf4j
// 核心配置,配置SpringSecurity访问策略,包括登录处理,登出处理,资源访问,密码基本加密。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //配置策略
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // post请求默认的都开启了csrf的模式,所有post请求都必须带有token之类的验证信息才可以进入登陆页面,此处是禁用csrf模式
        http.csrf().disable();
        http.formLogin()                    //  定义当需要用户登录时候,转到的登录页面。
            .and()
            .authorizeRequests()        // 定义哪些URL需要被保护、哪些不需要被保护
            .anyRequest()               // 任何请求,登录后可以访问
            .authenticated();
    }
     @Bean
    public BCryptPasswordEncoder passwordEncoder() { //密码加密
        return new BCryptPasswordEncoder(4);
    }
}

2.3访问

根据UserDetailsService实现类中设定的用户名和密码登录测试成功。

3.个性化认证逻辑

3.1.配置用户认证逻辑

@Service("userDetailsService")
public class CustomUserDetailService implements UserDetailsService {
    @Autowired
    private SysUserService userService;
    @Autowired
    private SysRoleService roleService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        // 从数据库中取出用户信息
        SysUser user = userService.selectByName(username);        
        // 判断用户是否存在
        if(user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        //根据用户获取其权限信息
        List<SysUserRole> userRoles = userRoleService.listByUserId(user.getId());
        for (SysUserRole userRole : userRoles) {
            SysRole role = roleService.selectById(userRole.getRoleId());
            authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
        }
        // 返回UserDes实现类user
        return new User(user.getUsername(), user.getPassword(), authorities);
    }
}

3.2.Spring Security相关配置

创建配置类,

  • 继承WebSecurityConfigurerAdapter,并重写它的方法来设置一些web安全的细节
@Configuration    //指明该类是一个配置类
@EnableWebSecurity  //开启 Security 服务
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启全局 Securtiy 注解,开启方法权限控制
@Slf4j
// 核心配置,配置SpringSecurity访问策略,包括登录处理,登出处理,资源访问,密码基本加密。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //
    @Autowired
    CustomUserDetailService userDetailService;
    //配置策略
    /** 放行静态资源 */
    @Override
    public void configure(WebSecurity web) throws Exception {
        //解决静态资源被拦截的问题
        web.ignoring().antMatchers("/static/**");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // post请求默认的都开启了csrf的模式,所有post请求都必须带有token之类的验证信息才可以进入登陆页面,此处是禁用csrf模式
        http.csrf().disable();
        http.authorizeRequests()    // 定义哪些URL需要被保护、哪些不需要被保护
            .antMatchers("/static/**").permitAll()   // 所有用户均可访问的资源
            .anyRequest()           // 任何尚未匹配的URL只需要验证用户即可访问
            .authenticated()       
            .and()
            .formLogin()            //  定义当需要用户登录时候,转到的登录页面。
            .loginPage("/login").permitAll()     // 设置登录页面
            .successHandler(loginSuccessHandler())  //自定义的登录接口
            .and()
            .logout().permitAll().invalidateHttpSession(true) //退出清空session
            .deleteCookies("JSESSIONID").logoutSuccessHandler(logoutSuccessHandler())
            .and()
            .sessionManagement().maximumSessions(10).expiredUrl("/login");
                
          //.usernameParameter("username")  // 自定义登陆用户名和密码参数,默认为username和password
          //.passwordParameter("password")
    }
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
        auth.eraseCredentials(false);
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() { //密码加密
        return new BCryptPasswordEncoder(4);
    }
    @Bean
    public LogoutSuccessHandler logoutSuccessHandler() { //登出处理
        return new LogoutSuccessHandler() {
            //第三个参数AuthenticationException,包括了登录失败的信息
            @Override
            public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                try {
                    SecurityUser user = (SecurityUser) authentication.getPrincipal();
                    log.info("USER : " + user.getUsername() + " LOGOUT SUCCESS !  ");
                } catch (Exception e) {
                    log.info("LOGOUT EXCEPTION , e : " + e.getMessage());
                }
                httpServletResponse.sendRedirect("/login");
            }
        };
    }

    @Bean
    public SavedRequestAwareAuthenticationSuccessHandler loginSuccessHandler() { //登入处理
        return new SavedRequestAwareAuthenticationSuccessHandler() {
            //该方法的第三个参数包含了登录用户的信息(UserDetails),Session信息,登录信息等
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                SysUser userDetails = (SysUser) authentication.getPrincipal();
                log.info("USER : " + userDetails.getUsername() + " LOGIN SUCCESS !  ");
                super.onAuthenticationSuccess(request, response, authentication);
            }
        };
    }
}

其中需注意,静态资源的访问不仅需要在此处配置,还需要添加如下配置

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
    }
}

3.3自定义登录页面

通过设置loginPage(""),可以是指定页面直接跳转或指定路径通过控制层返回。

3.4自定义处理登录成功/失败

在传统开发中,登录成功了都是进行了页面的跳转。 
在前后端分离的情况下,我们登录成功了可能需要向前端返回用户的个人信息,而不是直接进行跳转。登录失败也是同样的道理。这里涉及到了Spring Security中的两个接口AuthenticationSuccessHandler和AuthenticationFailureHandler。我们可以实现这个接口,并进行相应的配置就可以了。 当然框架是有默认的实现类的,我们可以继承这个实现类再来自定义自己的业务,在前面的配置类中我们就定义了匿名内部类的方式实现。

4.验证码

4.1图形验证码

SpringSecurity是通过过滤器链来进行校验的,我们想要验证图形验证码,所以可以在认证流程之前,也就是UsernamePasswordAuthenticationFilter之前进行校验。

4.1.1图形验证码整体流程大概是:
   1).页面初始化的时候服务器生成一个验证码,然后将验证码保存到session中,再显示给html(客户端)
   2).整合springsecurity校验,自定义一个filter,将该filter设置在UsernamePasswordAuthenticationFilter之前执行,这样就会在验证用户名密码之前就校验验证码
   3).我们自定义的filter里面校验html传来的验证码和第二步我们保存到session的验证码是否相同,如果相同那么就放行,验证成功

4.1.2具体实现


1.页面初始化的时候服务器生成一个验证码,然后将验证码保存到session中,再显示给html(客户端)

a.验证码信息

@Data
public class ImageCode {
    private BufferedImage image;
    private  String code;
    private LocalDateTime expireTime;//过期时间
    /**
     * @param image
     * @param code
     * @param expireInt :该参数是过期时间秒数,如60
     */
    public ImageCode(BufferedImage image, String code, int expireInt) {
        this.image = image;
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireInt);//当前时间加上60秒
    }
 }

b.验证码工具类

public class ImageCodeUtil{
    public static ImageCode createImageCode() {
        int width=80;
        int height=30;
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        Random random = new Random();
 
        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }
 
        String code = "";
        for (int i = 0; i < 4; i++) {
            String rand = String.valueOf(random.nextInt(10));
            code += rand;
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);
        }
 
        g.dispose();
 
        return new ImageCode(image, code, 60);
    }
    /**
     * 生成随机背景条纹
     * @param fc
     * @param bc
     * @return
     */
    private static Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
}

c.请求生成验证码(ImageCode)对象

@RestController
public class ImageCodeController {

    @RequestMapping(value = "/imageCode", method = RequestMethod.GET)
    public void imageCode(HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
        //生成imageCode对象
        ImageCode imageCode = ImageCodeUtil.createImageCode();
        //将图形验证码存入到session中
        request.getSession().setAttribute("imageCode", imageCode);
        // 将生成的图片写到接口的响应中
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
    }
}

2.整合springsecurity校验,自定义一个filter,将该filter设置在UsernamePasswordAuthenticationFilter之前执行,这样就会在验证用户名密码之前就校验验证码

a.自定义过滤器filter

@Component
public class ValidateCodefilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String uri= request.getRequestURI();
        //如果为get请求并且请求uri为/login(也就是我们登录表单的form的action地址)
        if( StringUtils.equalsIgnoreCase(request.getMethod(),"get") && StringUtils.containsIgnoreCase(request.getRequestURI(),"/login")) {
            logger.info("ValidateCodefilter执行了----" + "request.getRequestURI()=" + uri);
            //这里需要验证前端传过来的验证码是否和session里面存的一致,并且要判断是否过期
            logger.info(request.getSession().getAttribute("imageCode"));
            validateCode(request);
        }
    }
    /**
     * 验证用户输入的验证码和session中存的是否一致
     * @param request
     */
    private void validateCode(HttpServletRequest request) {

    }

}

此处需注意,StringUtils导包需要添加依赖

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.0</version>
</dependency>

b.在配置文件中的添加配置信息

    @Autowired
    private ValidateCodefilter validateCodefilter;
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
//将校验码验证放在UsernamePasswordAuthenticationFilter之前        
      http.addFilterBefore(validateCodefilter,UsernamePasswordAuthenticationFilter.class)
                .formLogin()                    //  定义当需要用户登录时候,转到的登录页面。
                .and()
                .authorizeRequests()        // 定义哪些URL需要被保护、哪些不需要被保护
                .anyRequest()               // 任何请求,登录后可以访问
                .authenticated();
    }

4.2短信验证码

短信验证码的功能实现,其实和图形验证码的原理是一样的。只不过一个是返回给前端一个图片,一个是给用户发送短消息,这里只需要去调用一下短信服务商的接口就好了。

但需要注意的是,在使用帐号密码登录的时候,UsernamePasswordAuthenticationToken里面包含了用户的帐号,密码,以及其他的是否可用等状态信息。我们是通过手机短信来做登录,所以就没有密码了,我们需要将验证流程中密码相关部分都进行修改,也即我们需要自定义认证过程中的一些类,包含 

  •  AuthenticationToken(流程默认为UsernamePasswordAuthenticationToken),
  • AuthenticationFilter(流程默认为UsernamePasswordAuthenticationFilter),
  • Provider(默认DaoAuthenticationProvider),

然后将自定义的相关类配置在configure方法中。

可参考:https://blog.csdn.net/u013435893/article/details/79684027

4.3 集成 Kaptcha 实现前后端分离验证码功能

 

4.4第三方登录

参考https://blog.csdn.net/u013435893/article/details/79735097

 

相关参考:

https://blog.csdn.net/yuanlaijike/article/category/9283872

https://blog.csdn.net/u013435893/article/category/7516861

三、Shiro

Shiro是Apache下的一个开源项目,我们称之为Apache Shiro。它是一个很易用与Java项目的的安全框架,提供了认证、授权、加密、会话管理,与spring Security 一样都是做一个权限的安全框架,但是与Spring Security 相比,在于 Shiro 使用了比较简单易懂易于使用的授权方式。shiro属于轻量级框架,相对于security简单的多,也没有security那么复杂。

 

 

 

 

 

 

 

 

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值