美文网首页spring bootjava 设计
Spring Security基本配置

Spring Security基本配置

作者: ruoshy | 来源:发表于2019-08-17 14:35 被阅读0次

    概述

      Spring Security是一个功能强大且可高度自定义的身份验证和访问控制框架。专注于为Java应用程序提供身份验证和授权的框架。与所有Spring项目一样,Spring Security的真正强大之处在于它可以轻松扩展以满足自定义要求。

    基本环境搭建

    创建一个Spring Boot Web项目在pom.xml中添加spring-boot-starter-security依赖即可

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

    添加完成后项目中的所有资源都会被保护起来

    添加一个简单的接口

    @RestController
    public class DemoController {
    
        @RequestMapping("/index")
        public String index() {
            return "Spring Security";
        }
    
    }
    

    启动成功后在浏览器中访问 /index 接口会自动跳转到登录页面,登录页面是由Spring security提供的,如图所示。

    login.png

    默认的用户名是:user,默认的登录密码是在每次启动项目时随机生成的,可在项目启动日志中查看。

    password.png

    登录成功后就可以正常访问接口了。

    配置用户名密码

    当对默认的用户名和密码不满意时可在配置文件中进行配置,如下:

    spring:
      security:
        user:
          roles: admin
          name: cwc
          password: 123456
    

    登录成功后用户还会具有一个角色——admin

    基于内存的认证

    我们也可以自定义类继承WebSecurityConfigurerAdapter,实现对Spring Security更多的自定义配置,例如基于内存的认证,如下:

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        /**
         * @return 密码编码器
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return NoOpPasswordEncoder.getInstance();
        }
        /**
         * @param auth 身份验证管理器
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication()
                    .withUser("user")
                    .password("123456")
                    .roles("admin")
                    .and()
                    .withUser("cwc")
                    .password("123456")
                    .roles("dba");
        }
    
    }
    

    Spring Security 5.x 中引入了多种密码加密方式必须指定一种,当前密码编码器使用的是 NoOpPasswordEncoder,即不对密码进行加密。

    基于数据库的认证

    由于基于内存的认证是定义在内存中的,在一般情况下用户的基本信息以及角色等都是存储在数据库中的,因此需要从数据库获取数据进行的认证和授权。

    首先需要设计一个基本的用户角色表,分别是用户表、角色表以及用户角色关联表

    table.png user.png
    角色名有一个默认的前缀 ROLE_

    数据库的配置以及表的实体类这里就不演示了,主要是在User 实体类中除了基本的geter/seter 还需要实现接口UserDetails

    public class User implements UserDetails {
        private Integer id;
        private String username;
        private String password;
        private boolean enabled;
        private boolean locked;
        private List<Role> roles;
     
        /**
         * 获取当前用户对象所具有的角色信息
         */
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            for (Role role : roles) {
                authorities.add(new SimpleGrantedAuthority(role.getName()));
            }
            return authorities;
        }
    
        /**
         * 当前账户是否未过期
         */
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        /**
         * 当前账户是否未锁定
         */
        @Override
        public boolean isAccountNonLocked() {
            return !locked;
        }
    
        /**
         * 当前账户密码是否未过期
         */
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        /**
         * 账户是否可用
         */
        @Override
        public boolean isEnabled() {
            return enabled;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    
        // 省略 getter/setter
    }
    

    创建UserService

    /**
     * 用户登录服务
     */
    @Service
    public class UserService implements UserDetailsService {
        @Resource
        private UserMapper userMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userMapper.loadUserByUsername(username);
            if (user == null) {
                throw new UsernameNotFoundException("账户不存在");
            }
            user.setRoles(userMapper.getRoleByUId(user.getId()));
            return user;
        }
    }
    

    创建UserMapper

    @Mapper
    public interface UserMapper {
    
        @Select("SELECT * FROM user where username = #{username}")
        User loadUserByUsername(String username);
    
        @Select("SELECT * FROM role r,user_role ur where r.id = ur.role_id and ur.user_id = #{id}")
        List<Role> getRoleByUId(Integer id);
    }
    
    

    在上面自定义的 WebSecurityConfig 类中重写 configure(AuthenticationManagerBuilder auth)方法。

        /**
         * @return 密码编码器
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        // 用户登录服务
        @Resource
        private UserService userService;
    
        /**
         * @param auth 身份验证管理器
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userService);
        }
    

    由于数据库中用户密码是通过BCryptPasswordEncoder类加密过的所以以上密码编码器已经改为BCryptPasswordEncoder。
    完成以上配置后,重启项目就可以使用保存在数据库中的用户名和密码进行登录并根据用户具备的角色进行授权。

    角色管理以及请求处理

    目前虽然已经可以实现认证功能,但是受保护的资源都是默认的无法根据实际情况进行角色管理,若需要实现这些功能可在上面自定义的 WebSecurityConfig 类中重写 configure(HttpSecurity http) 方法。

        // 登录成功处理
        @Resource
        private AuthenticationSuccessHandlerImpl authenticationSuccessHandler;
        // 登录失败处理
        @Resource
        private AuthenticationFailureHandlerImpl authenticationFailureHandler;
        // 注销处理
        @Resource
        private LogoutHandlerImpl logoutHandler;
        // 访问拒绝处理
        @Resource
        private AccessDeniedHandlerImpl accessDeniedHandler;
        // 身份验证入口点失败处理
        @Resource
        private AuthenticationEntryPointImpl authenticationEntryPoint;
    
        /**
         * @param http http安全处理
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    // 配置跨域资源共享
                    .cors()
                    .and()
                    // 授权请求
                    .authorizeRequests()
                    // 访问 /book/** 接口的请求必须具备 admin 角色
                    .antMatchers("/book/**").hasRole("admin")
                    // 访问 /brandlist/** 接口的请求必须具备 dba 角色
                    .antMatchers("/brandlist/**").hasRole("dba")
                    // 放行其他接口的请求
                    .anyRequest().permitAll()
                    .and()
                    // 登录接口的Url 可通过发起Post 请求进行登录
                    .formLogin().loginProcessingUrl("/login")
                    // 登录成功处理
                    .successHandler(authenticationSuccessHandler)
                    // 登录失败处理
                    .failureHandler(authenticationFailureHandler)
                    .and()
                    // 注销接口 默认Url 为/logout 可自定义
                    .logout()
                    // 注销处理
                    .addLogoutHandler(logoutHandler)
                    .and()
                    .exceptionHandling()
                    // 访问拒绝处理
                    .accessDeniedHandler(accessDeniedHandler)
                    // 身份验证入口点失败处理
                    .authenticationEntryPoint(authenticationEntryPoint);
        }
    

    为了使代码更具可读性可自定义了处理类来实现以下接口:

    • AuthenticationSuccessHandler(登录成功处理)
    • AuthenticationFailureHandler(登录失败处理)
    • LogoutHandler(注销处理)
    • AccessDeniedHandler(访问拒绝处理)
    • AuthenticationEntryPoint(身份验证入口点失败处理)

    源码如下:

    AuthenticationSuccessHandler
    @Component
    public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
            StatusMessage message = new StatusMessage();
            message.setMsg("登录成功!");
            message.setStatus(200);
            message.callback(response);
        }
    }
    
    AuthenticationFailureHandler
    @Component
    public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
            StatusMessage message = new StatusMessage();
            message.setStatus(401);
            if (e instanceof LockedException) {
                message.setMsg("账户被锁定,登录失败!");
            } else if (e instanceof BadCredentialsException) {
                message.setMsg("账户名或密码输入错误,登录失败!");
            } else if (e instanceof DisabledException) {
                message.setMsg("账户被禁用,登录失败!");
            } else if (e instanceof AccountExpiredException) {
                message.setMsg("账户已过期,登录失败!");
            } else {
                message.setMsg("登录失败!");
            }
            message.callback(response);
        }
    }
    
    LogoutHandler
    @Component
    public class LogoutHandlerImpl implements LogoutHandler {
        @Override
        public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
            StatusMessage message = new StatusMessage();
            message.setMsg("注销成功!");
            message.setStatus(403);
            try {
                message.callback(response);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    AccessDeniedHandler
    @Component
    public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException {
            StatusMessage message = new StatusMessage();
            message.setMsg("权限不足!");
            message.setStatus(403);
            message.callback(response);
        }
    }
    
    AuthenticationEntryPoint
    @Component
    public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
            StatusMessage message = new StatusMessage();
            message.setMsg("请求失败,请登录!");
            message.setStatus(403);
            message.callback(response);
        }
    }
    

    接口返回如下:

    login-1.png failure.png logout.png denied.png admin-api.png api-data.png

    单用户登录

        // 会话信息过期处理
        @Resource
        private SessionInformationExpiredStrategyImpl sessionInformationExpiredStrategy;
        // 会话注册器
        @Resource
        private CustomSessionRegistry customSessionRegistry;
        @Override
        protected void configure(HttpSecurity http) throws Exception {
                    http.
                    ...
                    .and()
                    .sessionManagement().maximumSessions(1)
                    // 会话注册器
                    .sessionRegistry(customSessionRegistry)
                    // 会话信息过期处理
                    .expiredSessionStrategy(sessionInformationExpiredStrategy);
        }
    
    SessionInformationExpiredStrategy

    自定义类 SessionInformationExpiredStrategyImpl实现 SessionInformationExpiredStrategy

    /**
     * 会话信息过期处理
     */
    @Component
    public class SessionInformationExpiredStrategyImpl implements SessionInformationExpiredStrategy {
        @Override
        public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException {
            StatusMessage message = new StatusMessage();
            message.setMsg("登录信息过期,可能是由于同一用户尝试多次登录!");
            message.setStatus(200);
            message.callback(sessionInformationExpiredEvent.getResponse());
        }
    
    }
    
    SessionRegistryImpl

    自定义类 CustomSessionRegistry继承 SessionRegistryImpl

    /**
     * 自定义会话注册器
     */
    @Component
    public class CustomSessionRegistry extends SessionRegistryImpl {
    
        /**
         * 获得用户Session信息
         *
         * @param user 用户信息
         */
        private List<SessionInformation> getSessionInformationList(User user) {
            // 获取父类会话注册器Session主体
            List<Object> users = this.getAllPrincipals();
            List<SessionInformation> sessionInformationList = new ArrayList<>();
            for (Object principal : users) {
                if (principal instanceof User) {
                    final User loggedUser = (User) principal;
                    if (user.getId().equals(loggedUser.getId())) {
                        // 返回该用户全部Session信息
                        sessionInformationList.addAll(this.getAllSessions(principal, false));
                    }
                }
            }
            return sessionInformationList;
        }
    
        /**
         * 若存在用户已登录对当前登录用户下线
         *
         * @param user 用户信息
         */
        public void invalidateSession(User user) {
            List<SessionInformation> sessionsInfo = this.getSessionInformationList(user);
            for (SessionInformation sessionInformation : sessionsInfo) {
                // 会话过期
                sessionInformation.expireNow();
            }
        }
    
    }
    

    完成以上配置后在UserService中注入自定义的类CustomSessionRegistry并添加:

    customSessionRegistry.invalidateSession(user);

    /**
     * 用户登录服务
     */
    @Service
    public class UserService implements UserDetailsService {
        @Resource
        private UserMapper userMapper;
    
        @Resource
        private CustomSessionRegistry customSessionRegistry;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 使用Mapper接口通过用户名获取用户信息
            User user = userMapper.loadUserByUsername(username);
            if (user == null) {
                throw new UsernameNotFoundException("账户不存在");
            }
            // 使用Mapper接口通过用户Id获取用户角色信息
            user.setRoles(userMapper.getRoleByUId(user.getId()));
            customSessionRegistry.invalidateSession(user);
            return user;
        }
    }
    
    

    重启项目即可以使用两个浏览器进行测试

    expire.png

    实例地址

    相关文章

      网友评论

        本文标题:Spring Security基本配置

        本文链接:https://www.haomeiwen.com/subject/wrzcsctx.html