基于Spring+SpringMVC+MyBatis博客系统的开发教程(十六)

第16课:Spring Security 之手机登录认证授权

通过上一篇的源码分析得知 Spring Security 提供的默认认证方式是根据用户名和密码进行认证的。要想通过手机登录认证就得制定自己的认证策略、认证逻辑以及获取用户信息的逻辑等。

自定义异常 PhoneNotFoundException

因为账号登录异常抛的是 UsernameNotFoundException 异常,那么手机登录认证失败我们就抛 PhoneNotFoundException。

在 security.phone 包下新建 PhoneNotFoundException 并继承 AuthenticationException,代码如下:

public class PhoneNotFoundException extends AuthenticationException {
    public PhoneNotFoundException(String msg, Throwable t) {
        super( msg, t );
    }

    public PhoneNotFoundException(String msg) {
        super( msg );
    }
}

主要是两个构造方法。里面调用的是父类的构造方法。接着往上查看会发现都继承自 RuntimeException 运行时异常。

自定义认证令牌 PhoneAuthenticationToken

账号登录使用的令牌是 UsernamePasswordAuthenticationToken,我们模仿它制定自己的 Token。

在 security.phone 包下新建 PhoneAuthenticationToken 并继承 AbstractAuthenticationToken:

public class PhoneAuthenticationToken extends AbstractAuthenticationToken {
    private final Object principal;

    public PhoneAuthenticationToken(Object principal) {
        super((Collection)null);
        this.principal = principal;
        this.setAuthenticated(false);
    }

    public PhoneAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    public Object getCredentials() {
        return null;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if(isAuthenticated) {
            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        } else {
            super.setAuthenticated(false);
        }
    }
    }

代码解读:

(1)一个参数的构造方法是将手机号赋值给 principal,然后权限设置为 null,认证状态为 false。

(2)两个参数的构造方法是传入权限集合、用户信息并将认证状态置为 true。

(3)因为我们没有用到密码。所以 getCredentials 返回 null。

自定义认证逻辑过滤器 PhoneAuthenticationFilter

在 security.phone 包下新建 PhoneAuthenticationFilter 并继承 AbstractAuthenticationProcessingFilter:

public class PhoneAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String phoneParameter = "telephone";
    public static final String codeParameter = "phone_code";
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    protected PhoneAuthenticationFilter( ) {
        super( new AntPathRequestMatcher("/phoneLogin") );
    }

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            String phone = this.obtainPhone(request);
            String phone_code = this.obtainValidateCode(request);
            if(phone == null) {
                phone = "";
            }
            if(phone_code == null) {
                phone_code = "";
            }

            phone = phone.trim();
            String cache_code = redisTemplate.opsForValue().get( phone );
            boolean flag = CodeValidate.validateCode(phone_code,cache_code);
            if(!flag){
                throw new PhoneNotFoundException( "手机验证码错误" );
            }
            PhoneAuthenticationToken authRequest = new PhoneAuthenticationToken(phone);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);

    }

    protected void setDetails(HttpServletRequest request, PhoneAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    protected String obtainPhone(HttpServletRequest request) {
        return request.getParameter(phoneParameter);
    }
    protected String obtainValidateCode(HttpServletRequest request) {
        return request.getParameter(codeParameter);
    }

    }

代码解读:

(1)将手机号和手机验证码的请求参数名分别赋值给 phoneParameter 和 codeParameter。

(2)通过 @Autowired 注解注入 RedisTemplate 对象。

(3)通过构造方法指定手机登录时的登录 URL 为 /phoneLogin

(4)通过自定义的 obtainPhone 和 obtainValidateCode 方法获取前台传来的手机号和手机验证码。

(5)获取 Redis 中存储的手机验证码并赋值给 cache_code,然后调用 CodeValidate 类中的 validateCode 方法判断用户输入的手机验证码是否正确。

(6)如果用户输入的手机验证码和 Redis 中存储的不一致则直接报 PhoneNotFoundException 异常,认证失败。

(7)实例化一个 PhoneAuthenticationToken 对象,然后设置请求信息,最后调用认证管理器找到支持该 Token 的 AuthenticationProvider 进行认证,并将认证的结果 Authentication 返回。

自定义获取用户信息逻辑的 PhoneUserDetailsService

在 security.phone 包下新建 PhoneUserDetailsService 并实现 UserDetailsService 接口:

 public class PhoneUserDetailsService implements UserDetailsService {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;

    public UserDetails loadUserByUsername(String phone) throws PhoneNotFoundException {
        User user = userService.findByPhone(phone);
        if(user == null){
            throw new PhoneNotFoundException("手机号码错误");
        }
        List<Role> roles = roleService.findByUid(user.getId());
        user.setRoles(roles);
        return user;
    }
    }

代码解读:

(1)通过 Autowired 注解注入 UserService 和 RoleService 对象。

(2)根据手机号查询用户 User,如果为 null 则直接抛 PhoneNotFoundException 异常,认证失败。

(3)用户不为 null,通过用户 id 获取用户的角色列表,将角色列表添加到用户 user 中,最后将 user 返回。

自定义手机登录认证策略 PhoneAuthenticationProvider

在 security.phone 包下新建 PhoneAuthenticationProvider 并实现 AuthenticationProvider 接口:

 public class PhoneAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        PhoneAuthenticationToken authenticationToken = (PhoneAuthenticationToken) authentication;
        UserDetails userDetails = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());

        if (userDetails == null) {

            throw new PhoneNotFoundException("手机号码不存在");

        } else if (!userDetails.isEnabled()) {

            throw new DisabledException("用户已被禁用");

        } else if (!userDetails.isAccountNonExpired()) {

            throw new AccountExpiredException("账号已过期");

        } else if (!userDetails.isAccountNonLocked()) {

            throw new LockedException("账号已被锁定");

        } else if (!userDetails.isCredentialsNonExpired()) {

            throw new LockedException("凭证已过期");
        }

        PhoneAuthenticationToken result = new PhoneAuthenticationToken(userDetails,
                userDetails.getAuthorities());

        result.setDetails(authenticationToken.getDetails());

        return result;
    }

    public boolean supports(Class<?> authentication) {
        return PhoneAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    }

代码解读:

(1)获取配置文件中配置的 UserDetailsService 对象。

(2)将 authenticationToken 对象强转为 PhoneAuthenticationToken 对象。

(3)调用 userDetailsService 对象的 loadUserByUsername 方法获取用户信息 UserDetails。

(4)如果报异常则认证失败。

(5)如果没有异常,则调用 PhoneAuthenticationToken 两个参数的构造方法,设置权限等,到这里则认证成功, 然后设置请求信息,并将认证结果返回。

(6)下面的 supports 方法中说明该 AuthenticationProvider 支持 PhoneAuthenticationToken 类型的 Token。

spring-security.xml 配置文件修改

在 spring-security.xml 配置文件中加入自定义的认证策略、认证逻辑过滤器等。部分配置如下,且未按顺序,具体配置请参考百度网盘中的配置文件:

 <security:custom-filter after="FORM_LOGIN_FILTER" ref="phoneAuthenticationFilter"/>
     <bean id="phoneAuthenticationFilter" class="wang.dreamland.www.security.phone.PhoneAuthenticationFilter">
        <property name="filterProcessesUrl" value="/phoneLogin"></property>
        <property name="authenticationManager" ref="authenticationManager"></property>
        <property name="sessionAuthenticationStrategy" ref="sessionStrategy"></property>
        <property name="authenticationSuccessHandler">
            <bean class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
                <property name="defaultTargetUrl" value="/list"></property>
            </bean>
        </property>
        <property name="authenticationFailureHandler">
            <bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
                <property name="defaultFailureUrl" value="/login?error=fail"></property>
            </bean>
        </property>
    </bean>

     <!-- 认证管理器,使用自定义的accountService,并对密码采用md5加密 -->
    <security:authentication-manager alias="authenticationManager">
        <security:authentication-provider user-service-ref="accountService">
            <security:password-encoder hash="md5">
                <security:salt-source user-property="username"></security:salt-source>
            </security:password-encoder>
        <security:authentication-provider ref="phoneAuthenticationProvider">
        </security:authentication-provider>
    </security:authentication-manager>
    <bean id="phoneService" class="wang.dreamland.www.security.phone.PhoneUserDetailsService"/>
     <bean id="phoneAuthenticationProvider" class="wang.dreamland.www.security.phone.PhoneAuthenticationProvider">
        <property name="userDetailsService" ref="phoneService"></property>
    </bean>

关于配置的说明之前已经介绍过。这里就不再赘述。

重新启动 Tomcat 测试

注意将登陆页面的手机登录 URL 改为 phoneLogin。

     <!--手机登录-->
     <div class="tab-pane fade" id="phone-login">
       <form role="form" class="login-form form-horizontal" id="phone_form" action="${ctx}/phoneLogin" method="post">
        ...

输入手机号和验证码后点击登录,手机登录认证测试成功!

如果输入错误的手机验证码,登录失败后跳转到了登录页面,但它跳转到的是账号登录选项卡。如果想让它跳转到手机登录选项卡,可自定义登录失败处理器。

1. 在 security.phone 包下新建 PhoneAuthenticationFailureHandler 并继承 SimpleUrlAuthenticationFailureHandler:

public class PhoneAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    private String defaultFailureUrl;

    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        String phone = request.getParameter("telephone");
        request.setAttribute("phoneError", "phone");
        request.setAttribute("phoneNum", phone);
        request.getRequestDispatcher(defaultFailureUrl).forward(request, response);
    }

    @Override
    public void setDefaultFailureUrl(String defaultFailureUrl) {
        this.defaultFailureUrl = defaultFailureUrl;
    }

    public String getDefaultFailureUrl() {
        return defaultFailureUrl;
    }
    }

代码解读:

(1)获取配置中配置的手机登录认证失败跳转 URL 赋值给 defaultFailureUrl。

(2)根据请求参数获取手机号。

(3)将 key="phoneError",value="phone" 设置到 Request 域中,由前台获取。

(4)将 key="phoneNum",value=phone 设置到 Request 域中,由前台获取。

(5)转发请求到 defaultFailureUrl。

2. 在 spring-security.xml 中配置自定义的认证失败处理器:

        <bean id="phoneAuthenticationFilter" class="wang.dreamland.www.security.phone.PhoneAuthenticationFilter">
        <property name="filterProcessesUrl" value="/phoneLogin"></property>
        <property name="authenticationManager" ref="authenticationManager"></property>
        <property name="sessionAuthenticationStrategy" ref="sessionStrategy"></property>
        <property name="authenticationSuccessHandler">
            <bean class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
                <property name="defaultTargetUrl" value="/list"></property>
            </bean>
        </property>
        <!--自定义登录失败处理器-->
        <property name="authenticationFailureHandler">
            <bean class="wang.dreamland.www.security.phone.PhoneAuthenticationFailureHandler">
                <property name="defaultFailureUrl" value="/login?error=fail"></property>
            </bean>
        </property>
    </bean>

3. 在 login.jsp 中创建页面加载完成函数:

    //页面加载完成函数
    $(function () {
        var msg = "${phoneError}";
        var phone = "${phoneNum}";
        if(msg == "phone"){
            $("#phone-login").attr("class","tab-pane fade in active")
            $("#p_login").attr("class","active");
            $("#account-login").attr("class","tab-pane fade");
            $("#a_login").attr("class","");
            $("#phone_span").text("短信验证码错误").css("color","red");
            $("#phone").val(phone);
        }
    });

代码解读:

(1)页面加载完成执行此函数,用 EL 表达式获取后台传来的 msg 和手机号。

(2)判断 msg 是不是字符串“phone”,如果是则显示手机登录选项 Tab,并且提示短信验证码错误,将用户的手机号回显到页面。

效果如图:

404、500错误页面配置

如果访问不存在的资源时会出现404错误,如果系统后台服务器出错会报500错误等待,如图404错误:

出现上面的页面对用户来说很不友好,我们配置自己的错误页面。

1. 在 web.xml 中引入404、500错误页面:

      <!-- 404页面 -->
    <error-page>
        <error-code>404</error-code>
        <location>/WEB-INF/404.jsp</location>
    </error-page>
    <!-- 500页面 -->
    <error-page>
        <error-code>500</error-code>
        <location>/WEB-INF/500.jsp</location>
    </error-page>

2. 在 webapp/WEB-INF/ 下引入 404.jsp 和 500.jsp 文件。

3. 将500错误页面用到的背景图片 bj.png 和图标 500.png 添加到 webapp/images 目录下,JSP 文件还有图片在百度网盘中下载。

404错误页面效果如下图:

500错误页面效果如下图:

第16课百度网盘地址:

链接:https://pan.baidu.com/s/1wJ93NTVkD_eKLB-rx1xjrg

密码:oihe

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

exodus3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值