基于Spring+SpringMVC+MyBatis博客系统的开发教程(十六)
通过上一篇的源码分析得知 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
本文来自博客园,作者:暗影月色程序猿,转载请注明原文链接:https://www.cnblogs.com/Geneling/p/15249200.html