精通Spring Boot——第十八篇:Spring Security 自定义认证流程

心不动则不痛 提交于 2020-04-14 14:33:23

【推荐阅读】微服务还能火多久?>>>

前两篇简单介绍了一下使用Spring Security 使用Http Basic登录,以及Spring Security如何自定义登录逻辑。这篇文章主要介绍如何使用handler来定义认证相关的流程。 先做一些自定义的操作,如配置自定义登录页,配置登录请求URL等。 当我们使用Spring Security时,它会为我们提供一个默认的登录页面,这显然没法满足我们的需求,那如何来自定义页面呢?请看代码:

/**
 * @author developlee
 * @since 2018/11/27 21:58
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final MyLoginHandler myLoginHandler;

    private final MyLogoutHandler myLogoutHandler;

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Autowired
    public SecurityConfig(MyLoginHandler myLoginHandler, MyLogoutHandler myLogoutHandler) {
        this.myLoginHandler = myLoginHandler;
        this.myLogoutHandler = myLogoutHandler;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义用户登录页,并允许客户端请求
        http.formLogin().loginPage("/login").permitAll()
                .loginProcessingUrl("/sign_in")
                // 配置登录成功的handler
                .successHandler(myLoginHandler)
                .and().authorizeRequests().anyRequest().authenticated();
        // 配置登出的handler
        http.logout().addLogoutHandler(myLogoutHandler)
                 // logout 成功,删除 cookies
         .deleteCookies("web-site", "custom-token").clearAuthentication(true);
                 // Spring Security 默认是开启了CSRF 保护的,所以logout操作必须是用POST方式请求,
                 // 如果非要使用GET请求来logout的话,也可以在代码中的实现
                 //.logoutRequestMatcher(new AntPathRequestMatcher("/logout","GET"))
        //session管理   session失效后跳转
        http.sessionManagement().invalidSessionUrl("/login");
        //只允许一个用户登录,如果同一个账户两次登录,那么第一个账户将被踢下线,跳转到登录页面
        http.sessionManagement().maximumSessions(1).expiredUrl("/login");
    }

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

接下来写个简单的登录页,这个页面我是用thymeleaf模板写的,也是第一次使用thymeleaf,还请大家多多包涵这丑陋的画风。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf-Login-Demo</title>
</head>
<body>
    <div id="header">
        <h2>登录示例页面-供参考</h2>
        <strong>demo login page for example</strong>
    </div>
    <div id="container">
        <form th:action="@{/sign_in}" method="post">
            <input name="username" type="text" placeholder="用户名"/>
            <br/>
            <input name="password" type="password" placeholder="密码"/>
            <br/>
            <input name="登录" type="submit" />
            <br/>
        </form>
    </div>
</body>
</html>

项目启动起来,看到的页面效果图如下:

结合数据库来实现用户登录,按照我们的思路,实现UserDetails, UserDetailsService 这两接口。首先,让我们自己的User实体类实现UserDetails接口. 自定义User实体类,这个类和我们的数据库结构是对应的。

/**
 * @author developlee
 * @since 2018/11/27 21:38
 */
@Entity
@Table(name = "tb_users")
@Data
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "username")
    private String username;

    @Column(name = "password")
    private String password;

    @Column(name = "age")
    private String age;

    @Column(name = "sex")
    private String sex;

    @Column(name = "isLock")
    private boolean isLock;

    @Column(name = "isEnabled")
    private boolean isEnabled;

}

实现UserDetailsService接口,重写loadUserByUsername方法

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库查询用户
        User user = userRepository.findByUsername(username);
        if(user == null) {
             throw new UsernameNotFoundException("用户" + username + "不存在");
        }
        return new MyUserDetails(user);
    }
}

接下来,自定义MyUserDetails实现UserDetails接口,构造方法传入我们从数据库查询出来的User对象。

/**
 * @author developlee
 * @since 2018/11/27 21:42
 */
public class MyUserDetails implements UserDetails {

    private User user;

    public MyUserDetails(User user) {
        this.user = user;
    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return user.isLock();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return user.isEnabled();
    }
}

搞个登录成功的处理器MyLoginHandler,登录成功后,打印一行日志,并跳转到hello页

@Slf4j
@Component
public class MyLoginHandler implements AuthenticationSuccessHandler {

    // 登录成功处理
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        log.info("登录成功!");
        httpServletResponse.sendRedirect(httpServletRequest.getContextPath().concat("/hello"));
    }
}

登录试试看吧,见证奇迹的时刻 这是数据库中创建的用户

生成密码的代码

  @Autowired
    private PasswordEncoder passwordEncoder;

    @Test
    public void testMac() {
        String password = "123456";
        System.out.println("加密后密码:" + passwordEncoder.encode(password));
    }

密码生成插入数据库,应该在用户注册时进行操作。

然后,让我们在hello页,新增一个logout按钮,来实现登出功能。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>
    <p>Hello, World!</p>
    <a id="logout-btn" th:href="@{/sign_out}">我要退出!</a>
</body>
</html>

点击‘我要退出!’即可跳转到登录页! 现在登录登出我们都已经准备就绪,接下来,为我们的登录加些料吧! 默认的登出链接是/logout,如果开启了CSRF验证(默认是开启的),则该登出请求,必须设置为post请求。登出后浏览器跳转路径模式/login?logout。SecurityContextLogoutHandler 默认是作为最后的logoutHandler的。在处理登出请求中,我们可以自己添加logoutHandler或者LogoutSuccessHandler的实现。接下来请看代码演示: logoutHandler处理器

@Slf4j
@Component
public class MyLogoutHandler implements LogoutHandler {
    @Override
    public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
        log.info("登出成功了!!!");
        authentication.setAuthenticated(false); // 设置为未授权
    }
}

本文的所有代码我已经放在我的github.com上,感谢您的观看,如果有什么错误的地方,还请指出,共同探讨!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!