Spring Security 注册登录功能的基础实现

在java安全框架方面,除了shiro,还有一个很强大的Spring Security。
它们可以帮我们执行身份验证、授权、密码和会话管理。
本次我使用Spring Security来完成前后端分离情况下的注册、登录、注销、未登录访问后台、登录后访问后台等功能。
这5大功能基本完成了一个简单的登录认证。

0. 使用

在创建SpringBoot项目时就可以勾选上SpringSecurity,在POM中会有如下依赖:

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

之后我们需要创建一个类继承WebSecurityConfigurerAdapter并重写configure(HttpSecurity http)方法。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers( "/register").permitAll()
                .anyRequest().authenticated();
    }

}

现在只考虑注册吧,将”/register”请求允许所有人访问。
后面的权限控制,都在该方法中添加。

1. 注册

注册得自己实现,创建一个Controller和Service,将用户insert进用户表即可。

    @PostMapping("/register")
    public BaseResponse register(@Valid @ModelAttribute UserRegisterReq req) {
        userService.registerUser(req);
        return BaseResponseBuilder.success();
    }

唯一要注意的是密码,我们肯定不能保存明文,如何加密别人也已经帮我做好了。
这里我们使用BCryptPasswordEncoder,帮助我们进行加密。

    @Override
    public void registerUser(UserRegisterReq req) {
        User user = new User();
        BeanUtils.copyProperties(req, user);
        user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
        userRepository.save(user);
    }

这里是个简易版的,实际在添加用户时还有其他的业务操作,我们到此就算完成了。
尝试访问此接口,看看数据库是否能正常插入,以及看看密码是什么样。

2. 登录

首先需要让框架知道我们需要登录功能,所以添加:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers( "/register").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login").permitAll().successHandler(loginSuccessHandler()).failureHandler(loginFailureHandler());
    }

在登录时,我们肯定需要将对应的用户查出来,交给SpringSecurity框架,所以我们需要实现UserDetailsService接口的loadUserByUsername方法。在SecurityConfig中添加:

    @Bean
    public UserDetailsService myUserDetailsService() {
        return new UserDetailsService() {
            @Autowired
            private UserRepository userRepository;

            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                // 从数据库查询用户信息
                MyUser myUser = userRepository.getUser(username);
                if (myUser != null) {
                    List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
                    simpleGrantedAuthorities.add(new SimpleGrantedAuthority("USER"));
                    return new User(username, myUser.getPassword(), simpleGrantedAuthorities);
                } else {
                    throw new UsernameNotFoundException("用户名或密码错误");
                }
            }
        };
    }

此处我们可以看到,我们给用户设置了USER权限,本次我们不涉及权限所以这个以后再说。
我们返回UserDetails给框架即可。

重写了UserDetailsService我们还得让框架知道才行。
另外之前注册我们使用了BCryptPasswordEncoder,所以为了SpringSecurity框架在验证时能够正常解密,所以我们需要在SecurityConfig中添加:

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

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

现在问题来了,我们现在前后端分离了,而SpringSecurity默认是返回的一个页面,怎么处理呢?
我们可以分别实现AuthenticationSuccessHandler和AuthenticationFailureHandler。

    @Bean
    public AuthenticationSuccessHandler loginSuccessHandler() {
        return new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                User userDetails = (User) authentication.getPrincipal();
                // 自定义登录成功后的返回
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = httpServletResponse.getWriter();
                out.write(JSONObject.toJSONString(BaseResponseBuilder.success("登录成功", result)));
                out.flush();
            }
        };
    }

    @Bean
    public AuthenticationFailureHandler loginFailureHandler() {
        return new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                // 自定义登录失败的返回
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = httpServletResponse.getWriter();
                out.write(JSONObject.toJSONString(BaseResponseBuilder.failure("用户名或密码错误")));
                out.flush();
            }
        };
    }

具体返回的逻辑还得自己根据业务实现。
再使用之前注册的账号,尝试访问一下接口。

3. 注销

首先需要让框架知道我们需要注销功能,所以添加:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers( "/register").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login").permitAll().successHandler(loginSuccessHandler()).failureHandler(loginFailureHandler())
                .and()
                .logout().logoutSuccessHandler(logoutSuccessHandler()).permitAll();
    }

和注册类似,我们可以自己实现注销成功的操作。
实现LogoutSuccessHandler接口即可。

    @Bean
    public LogoutSuccessHandler logoutSuccessHandler() {
        return new LogoutSuccessHandler() {
            @Override
            public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                // 自定义注销成功的返回
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = httpServletResponse.getWriter();
                out.write(JSONObject.toJSONString(BaseResponseBuilder.success("退出成功")));
                out.flush();
            }
        };
    }

注销的默认地址是“/logout”,需要使用POST方法,可以自己调用下接口试试。

4. 未登录访问后台

未登录的用户不能访问后台,如果有未登录的用户访问后台接口,需要提示未登录。
需要实现AuthenticationEntryPoint接口。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .httpBasic().authenticationEntryPoint(authenticationEntryPoint())
                .and()
                .authorizeRequests()
                .antMatchers( "/register").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login").permitAll().successHandler(loginSuccessHandler()).failureHandler(loginFailureHandler())
                .and()
                .logout().logoutSuccessHandler(logoutSuccessHandler()).permitAll();
    }

    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return new AuthenticationEntryPoint() {
            @Override
            public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                // 用户访问无权限资源时的异常处理
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = httpServletResponse.getWriter();
                out.write(JSONObject.toJSONString(BaseResponseBuilder.failure("请先登录")));
                out.flush();
            }
        };
    }

之后可以找一个后台的接口地址访问一下,看看返回是否正常。

5. 登录后访问后台

先访问登录接口,返回“登录成功”后,再访问之前后台的接口,看看返回结果是什么?
你会发现,在4中提示我们“请先登录”,而在访问登录接口后,就能正常返回数据了。
这个过程我们并没有写过任何的代码,都是SpringSecurity框架默认实现的一套逻辑。
查看登录接口的返回包的headers,有个set-cookie字段,cookie肯定不需要我多做介绍了。
之后的请求,都会带上对应的cookie,服务端就能够“认识”我们了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据