第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错误页面:
< error-page >
< error-code > 404</ error-code >
< location > /WEB-INF/404.jsp</ location >
</ error-page >
< 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