栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Spring Security

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Spring Security

对于权限的管理,在企业应用程序的开发中,是必不可少的功能,但是能够灵活且强大的权限控制又不是一件容易的事情,所以在自己学习编写权限控制体系的基础上也接触一下成熟的框架,Spring 的全家桶系列 Spring Security 就进入了我们的视线。

Spring Security,这是一种基于 Spring AOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。

  • 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是那个用户。
  • 授权:经过认证后判断当前用户是否有权限进行某个操作。

而认证和授权也是 Spring Security 作为安全框架的核心功能。

入门案例

我们通过一个简单的入门案例来了解 Spring Security 。

引入依赖

我们已父子项目方式搭建,所有的依赖都在父项目中书写,子项目只需继承父项目即可。父项目中的写法:


    org.springframework.boot
    spring-boot-starter-parent
    2.7.0


    
       org.springframework.boot
       spring-boot-starter-web
    
            
       org.projectlombok
       lombok
    
    
       org.springframework.boot
       spring-boot-starter-security
    
控制类

书写一个普通的 controller 类

@RestController
@RequestMapping("/guanwei")
public class GuanweiController {

    @GetMapping
    public String execute() {
        return "This is GuanweiController`execute!";
    }
}
启动类 
@SpringBootApplication
public class Security01Application {
    public static void main(String[] args) {
        SpringApplication.run(Security01Application.class, args);
    }
}
运行效果

当我们还按照过去的方式访问 http://localhost:8081/guanwei 路径时,发现弹出了登录框,这个是 Spring Security 自带的登录页面,要求我们必须登录后才能访问系统资源。 

并且在项目启动后,在控制台能看到初始生成的密码。

使用 Spring Security 后访问系统任意资源时,就会跳转到默认登录页面,默认账号是 user ,登录成功后,才能访问才能对目标接口进行访问。以后只要不关闭浏览器或者服务器,都可以直接访问。也可以手动登出,路径是:logout 。 

登陆认证

我们刚才通过一个简单的案例,了解了 Spring Security 的基本概念。也发现 Spring Security 会在服务器启动时随机生成密码,那么有的童鞋就会想到,能不能自己去定义这个密码,甚至于使用数据库来校验用户登陆。

自定义账号密码

这种方式其实很简单,只需要在配置文件 (application.yml) 中设置账号密码就行。

spring:
  security:
    user:
      name: root
      password: guanwei

还可以在配置类中进行设置,注释上边的写法,在 config 包下创建一个配置类 SecurityConfig 来配置账号密码信息。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String pass = passwordEncoder.encode("123456");
        auth.inMemoryAuthentication().withUser("guanwei").password(pass).roles("admin");
    }
    @Bean
    PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

BCryptPasswordEncoder 类是 Spring Security 中的一个加密方法类。BCryptPasswordEncoder 方法采用了 SHA-256+随机盐+密钥对密码进行加密。 SHA 是一种安全 Hash 函数(SHA),是使用最广泛的Hash函数。
加密算法与 hash 算法的区别:

  • 加密算法是可逆的,加密算法的基本过程是对原来为明文的数据按某种算法进行处理,使其成为不可读的一段代码为“密文”,但在用相应的密钥进行操作之后就可以得到原来的内容 。
  • hash 算法是一种单向密码体制,即它是一个从明文到密文的不可逆的映射,只有加密过程,没有解密过程。同时,哈希函数可以将任意长度的输入经过变化以后得到固定长度的输出。
数据库校验方式

前面两种都是写死的,可能对于固定的超级管理员可以用,我们真实的项目场景肯定都是数据库里面的,肯定需要自定义查询,我们先把SecurityConfig 注释了,重新创建一个类 SpringSecurityConfig 来从数据库中判断账号密码。

引入数据库相关依赖


     mysql
     mysql-connector-java
     runtime


     com.baomidou
     mybatis-plus-boot-starter
     3.5.1


     com.alibaba
     druid-spring-boot-starter
     1.2.9

书写 bean 包的类

@Data
public class Users implements Serializable {
    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String userName;
    private String passWord;
    private String nickName;
}

书写 mapper 包的类

@Mapper
public interface UsersMapper extends BaseMapper {
}

书写一个 service 类继承 UserDetailsService 

@Service
public class LoginUsersServiceImpl implements UserDetailsService {
    @Resource
    private UsersMapper mapper;

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper query = new QueryWrapper<>();
        query.eq("username", username);
        Users users = mapper.selectOne(query);
        // 如果没有找到用户名 抛出异常 失败
        if (users == null) {
            System.out.println("账号或者密码错误");
            throw new UsernameNotFoundException("账号或者密码错误");
        }
        // 这里是加载用户权限,这里先模拟个 admin 权限,更详细的在后边会说到
        List auths = AuthorityUtils.createAuthorityList("admin");
        return new User(username, new BCryptPasswordEncoder().encode(users.getPassWord()), auths);
    }
}

书写 SpringSecurityConfig 配置类

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Resource
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
    }
}

这时候继续访问刚才的页面,输入数据库的账号和密码就行了。

自定义登录页面

我们刚才一直使用 security 自带的登陆页面,但是在实际使用中我们更多的是使用我们自己书写的登陆页面,要想设置其实很简单,如下几步就行。

创建登录页面


账号:
密码:

在配置类中设置登录页面 

    // 自定义登录页面
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()   //自定义登陆页面
                .loginPage("/login.html") //登录页面设置
                .loginProcessingUrl("/user/login")     //登陆访问路径  这里同 action
                .defaultSuccessUrl("/index.html").permitAll()   //登陆成功之后,跳转路径
                .and().authorizeRequests()
                .antMatchers( "/login.html", "/user/login").permitAll()  //设置哪些路径可以直接访问,不需要认证
                .anyRequest().authenticated()   //所有请求都可以访问
                .and().csrf().disable();  //关闭csrf
    }
授权管理

在项目中,有很多接口是针对不同角色权限的,如果角色是超级管理员,就拥有访问所有权限的能力,如果不是超级管理员,访问其他接口是不能允许的。 有些用户具有部分权限,就可以访问这些权限所能访问的内容,如果要实现这种效果,我们就需要授权管理。

基于权限

hasAuthority()方法:如果当前的主体具有指定的权限,则返回 true,否则返回 false。

配置类

protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()   //自定义登陆页面
                .loginPage("/login.html") //登录页面设置
                .loginProcessingUrl("/user/login")     //登陆访问路径  这里同 action
                .defaultSuccessUrl("/index.html").permitAll()   //登陆成功之后,跳转路径
                .and().authorizeRequests()
                .antMatchers( "/login.html", "/user/login").permitAll()  //设置哪些路径可以直接访问,不需要认证
                .antMatchers("/index.html").hasAuthority("guanwei") // 设置当前登录用户访问index.html页面时需要guanwei权限
                // 设置当前登录用户访问1.html页面时需要manager1、manager2、manager3或manager4权限
                .antMatchers("/1.html").hasAnyAuthority("manager1","manager2","manager3","manager4")
                .anyRequest().authenticated()   //所有请求都可以访问
                .and().csrf().disable();  //关闭csrf
    }

  

这里我们可以看到访问 index.html 页面需要 guanwei 权限,而访问1.html 只要有 manager1、manager2、manager3 或 manager4 权限之一就行。

业务类

还是使用刚才的权限 guanwei,然后看看运行结果。

运行结果

访问 index.html 页面

访问 1.html 页面

我们更换权限为 manager1 来试一试

这里就可以直接访问了。 

基于角色

配置类

业务类

运行结果

配置 403 页面

当没有权限的时候访问会报403,我们可以自定义一个页面。

创建一个 403.html 页面




    
    403


你没有相应的权限访问这个页面!

配置类中设置

运行结果

  

用户注销

SpringSecurity 的注销功能很简单,只需要一个超链接地址为 /logout 就行。




    
    Title


我是注销页面!
注销

分离项目

通过刚才的案例我们了解到了 SpringSecurity 认证授权的效果。可是刚才通过的是表单方式进行提交,在其后的项目中我们可能更多的使用前后端分离效果,那么我们就要返回给前端对应的消息来告诉前端认证授权的情况。

前期准备

前后端分离项目,需要后端返回消息来标注相应状态,这时候我们创建三个类来对返回的消息统一格式。

统一返回格式

@Data
public class JsonResult implements Serializable {
    private Boolean success;
    private Integer errorCode;
    private String errorMsg;
    private T data;
    public JsonResult() {
    }
    public JsonResult(boolean success) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
    }

    public JsonResult(boolean success, ResultCode resultEnum) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
    }

    public JsonResult(boolean success, T data) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
        this.data = data;
    }

    public JsonResult(boolean success, ResultCode resultEnum, T data) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
        this.data = data;
    }
}

返回状态码定义


public enum ResultCode {
    
    SUCCESS(200, "成功"),

    
    COMMON_FAIL(999, "失败"),

    
    PARAM_NOT_VALID(1001, "参数无效"),
    PARAM_IS_BLANK(1002, "参数为空"),
    PARAM_TYPE_ERROR(1003, "参数类型错误"),
    PARAM_NOT_COMPLETE(1004, "参数缺失"),

    
    USER_NOT_LOGIN(2001, "用户未登录"),
    USER_ACCOUNT_EXPIRED(2002, "账号已过期"),
    USER_CREDENTIALS_ERROR(2003, "密码错误"),
    USER_CREDENTIALS_EXPIRED(2004, "密码过期"),
    USER_ACCOUNT_DISABLE(2005, "账号不可用"),
    USER_ACCOUNT_LOCKED(2006, "账号被锁定"),
    USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"),
    USER_ACCOUNT_ALREADY_EXIST(2008, "账号已存在"),
    USER_ACCOUNT_USE_BY_OTHERS(2009, "账号下线"),

    
    NO_PERMISSION(3001, "没有权限");
    private Integer code;
    private String message;

    ResultCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    
    public static String getMessageByCode(Integer code) {
        for (ResultCode ele : values()) {
            if (ele.getCode().equals(code)) {
                return ele.getMessage();
            }
        }
        return null;
    }
}

返回体构造工具

public class ResultTool {
    public static JsonResult success() {
        return new JsonResult(true);
    }

    public static  JsonResult success(T data) {
        return new JsonResult(true, data);
    }

    public static JsonResult fail() {
        return new JsonResult(false);
    }

    public static JsonResult fail(ResultCode resultEnum) {
        return new JsonResult(false, resultEnum);
    }
}
未登录效果

当用户未登录时,会自动进入当前类的 commence 方法,我们在这个方法中返回 JSON 格式的错误信息。

@Component
public class NotLoginAuthentication implements AuthenticationEntryPoint {
    // 当用户未登录时 访问资源会返回结果
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        JsonResult result = ResultTool.fail(ResultCode.USER_NOT_LOGIN);
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(result));
    }
}
登录失败效果

当用户登录失败时(不论是账号未找到,密码错误还是权限问题),都会进入这个类的指定方式,我们在这个方法中返回 JSON 格式的错误信息。

@Component
public class FailureAuthenticationHandler implements AuthenticationFailureHandler {

    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) 
                                    throws IOException, ServletException {
        //返回json数据
        JsonResult result;
        if (e instanceof AccountExpiredException) {
            //账号过期
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED);
        } else if (e instanceof BadCredentialsException) {
            //密码错误
            result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR);
        } else if (e instanceof CredentialsExpiredException) {
            //密码过期
            result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED);
        } else if (e instanceof DisabledException) {
            //账号不可用
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE);
        } else if (e instanceof LockedException) {
            //账号锁定
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED);
        } else if (e instanceof InternalAuthenticationServiceException) {
            //用户不存在
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST);
        } else {
            //其他错误
            result = ResultTool.fail(ResultCode.COMMON_FAIL);
        }
        //处理编码方式,防止中文乱码的情况
        response.setContentType("text/json;charset=utf-8");
        //塞到response中返回给前台
        response.getWriter().write(JSON.toJSONString(result));
    }
}
登录成功效果

同样的道理,对正确的消息返回 JSON 格式信息。

@Component
public class SuccessAuthenticationHandler implements AuthenticationSuccessHandler {

    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //返回json数据
        JsonResult result = ResultTool.success();
        //处理编码方式,防止中文乱码的情况
        response.setContentType("text/json;charset=utf-8");
        //塞到response中返回给前台
        response.getWriter().write(JSON.toJSONString(result));
    }
}
配置文件

在配置文件中注册三个效果。

protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .permitAll()
                .successHandler(successHandler)  //登录成功处理逻辑
                .failureHandler(failureHandler) //登录失败处理逻辑
                //异常处理(权限拒绝、登录失效等)
                .and()
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)//匿名用户访问无权限资源时的异常处理
                .and()
                .authorizeRequests()
                .antMatchers("/login.html", "/login").permitAll()
                .antMatchers("/dailyblue/guanwei").hasAnyAuthority("admin")
                .and().csrf().disable();  //关闭csrf

    }
APIPost测试

登录成功的情况

登录失败的情况

没有登录就访问其他资源情况

SpringSecurity 基本流程

SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器,这里我们可以看看入门案例中的过滤器。

SpringSecurity 拦截器

1 . org.springframework.security.web.context.SecurityContextPersistenceFilter
     首当其冲的一个过滤器,作用之重要,自不必多言。
     SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个
     SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。
     SecurityContext中存储了当前用户的认证以及权限信息。
2 . org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
     此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager
3 . org.springframework.security.web.header.HeaderWriterFilter
    向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
4 . org.springframework.security.web.csrf.CsrfFilter
    csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,
     如果不包含,则报错。起到防止csrf攻击的效果。
5. org.springframework.security.web.authentication.logout.LogoutFilter
    匹配 URL为/logout的请求,实现用户退出,清除认证信息。
6 . org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
    认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。
7 . org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
    如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。
8 . org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
     由此过滤器可以生产一个默认的退出登录页面
9 . org.springframework.security.web.authentication.www.BasicAuthenticationFilter
    此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。
10 . org.springframework.security.web.savedrequest.RequestCacheAwareFilter
    通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest
11 . org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
   针对ServletRequest进行了一次包装,使得request具有更加丰富的API
12 . org.springframework.security.web.authentication.AnonymousAuthenticationFilter
   当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。
   spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
13 . org.springframework.security.web.session.SessionManagementFilter
   SecurityContextRepository限制同一用户开启多个会话的数量
14 . org.springframework.security.web.access.ExceptionTranslationFilter
   异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常
15 . org.springframework.security.web.access.intercept.FilterSecurityInterceptor
   获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。

WebSecurityConfigurerAdapter 接口
@Configuration
public class BrowerSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired  //自定义的安全元   数据源     实现FilterInvocationSecurityMetadataSource
    private MyInvocationSecurityMetadataSourceService myInvocationSecurityMetadataSourceService;
    @Autowired //自定义访问决策器
    private MyAccessDecisionManager myAccessDecisionManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        
        http.formLogin()
                .loginPage("")                      //登录页面                      /login
                .passwordParameter("")              //设置form表单中对应的name参数  默认为 password   下同
                .usernameParameter("")              //
                .defaultSuccessUrl("")            //认证成功后的跳转页面 默认跳转页面   可以设置是否总是默认  不是的话可以跳转与用户的target-url
                .failureUrl("")
                .failureForwardUrl("")            //登录失败 转发 的url
                .successForwardUrl("")              //登录成功 转发 的url  与successHandler对应  即处理完后请求转发的url
                .failureHandler(null)               //自定义的认证失败 做什么处理
                .successHandler(null)               //自定义认证成功 后做的处理    ----- 例如 想记录用户信息判断用户状态等
                .permitAll()                      //对于需要所有用户都可以访问的界面 或者url进行设置
                .loginProcessingUrl("")             //自定义处理认证的url    默认为    /login
                .authenticationDetailsSource(null)  //自定义身份验证的数据源  理解为查出数据库中的密码 和权限(可以不加) 然后再交给security
                修改和替换配置     已经配置好的修改   例如下面修改  安全拦截器的安全数据源
                .withObjectPostProcessor(new ObjectPostProcessor() {
                    public  O postProcess(
                            O fsi) {
                        fsi.setPublishAuthorizationSuccess(true);
                        //修改成自定义的     安全元数据源  权限的源  !!!!!
                        fsi.setSecurityMetadataSource(myInvocationSecurityMetadataSourceService);
                        //修改成自定义的     访问决策器  自定义的
                        fsi.setAccessDecisionManager(myAccessDecisionManager);
                        //使用系统的
                        fsi.setAuthenticationManager(authenticationManager);
                        return fsi;
                    }
                });
        
        http.authorizeRequests()
                .antMatchers("url匹配路径").permitAll()          //url匹配路径 permitAll 运行 全部访问 不用认证
                .accessDecisionManager(null)                                 //访问决策器
                .filterSecurityInterceptorOncePerRequest(true)               //过滤每个请求一次的安全拦截器 ???
                .anyRequest().authenticated()                                //其他的请求 需要认证,
                .antMatchers("/admin
        http.anonymous().disable().csrf().disable();                         //禁止匿名  关闭csrf
        
        http.logout()                                                        //登出处理
                .logoutUrl("/my/logout")
                .logoutSuccessUrl("/my/index")
                .logoutSuccessHandler(null)
                .invalidateHttpSession(true)
                .addLogoutHandler(null)
                .deleteCookies("cookieNamesToClear")
        ;
        
        http.sessionManagement()                                            //session管理
                .maximumSessions(2)                                         //最大session 数量 --用户
                .maxSessionsPreventsLogin(false)                            //超过最大sessin数量后时候阻止登录
                .expiredUrl("/")                                            //会话失效后跳转的url
                .expiredSessionStrategy(null)                               //自定义session 过期错略
                .sessionRegistry(null)                                     //自定义的session 注册 表
        ;

    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        
        auth
                
                .authenticationEventPublisher(null)
                
                .userDetailsService(null)
                
                .passwordEncoder(null)
        ;
    }

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

    @Override
    public void configure(WebSecurity web) throws Exception {
        
        web.ignoring().mvcMatchers();
        
        web.addSecurityFilterChainBuilder(null);
    }
}

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/998809.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号