一、文章导读
SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。当我们使用SpringSecurity自定义登录页面的时候,如果发生登陆失败的情况,如何在自己的页面上显示相应的错误信息?
针对上面所描述的问题,本次我们将通过具体的案例来将问题进行描述并提出对应的解决方案,我们将从如下方面进行具体讲解:
· SpringSecurity环境搭建
· 更换自己的登录页面
· 在登录页面显示错误信息
· 自定义用户名不存在的错误信息
· 自定义用户密码错误的错误信息
二、SpringSecurity环境搭建
1.创建maven工程项目
使用IDEA进行Maven项目的创建,并将项目构建为web项目,创建方式如下:
2.在项目的pom.xml添加依赖
<dependencies>
<!--SpringMVC与Spring相关jar包略 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>4.1.0.RELEASE</version>
</dependency>
</dependencies>
3.web.xml添加委托过滤器代理类
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
4.添加springSecurity的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!--配置放行资源 -->
<security:http pattern="/login.html" security="none"/>
<security:http pattern="/js/**" security="none"/>
<!--配置拦截规则 -->
<security:http use-expressions="false">
<security:intercept-url pattern="/**" access="ROLE_ADMIN"/>
<!--采用默认springSecurity提供的默认登陆页面 -->
<security:form-login/>
<security:csrf disabled="true"/>
</security:http>
<security:authentication-manager>
<security:authentication-provider user-service-ref="userDetailService"/>
</security:authentication-manager>
<bean id="userDetailService" class="UserDetailService实现类"></bean>
</beans>
5.添加UserDetailsService与TbUser
UserService类
public class UserService implements UserDetailsService {
public static Map<String, TbUser> users = new HashMap<String, TbUser>();
static {
users.put("root", new TbUser(1, "root", "root", "ROOT"));
users.put("admin", new TbUser(1, "admin", "admin", "ADMIN"));
}
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
if (users.containsKey(username)) {
//查询到用户信息
TbUser tbUser = users.get(username);
List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
list.add(new SimpleGrantedAuthority(tbUser.getRole()));
return new User(username, tbUser.getPassword(), list);
} else {
//未查询到用户信息
return null;
}
}
}
TbUser类
public class TbUser {
private Integer id;
private String username;
private String password;
private String role;
//setter...getter...constructor...省略
}
6.运行测试
三、更换自己的登录页面
1.修改springsecurity.xml
<security:http use-expressions="false">
<security:intercept-url pattern="/**" access="ROLE_ADMIN"/>
<!--采用默认springSecurity提供的默认登陆页面 -->
<!--<security:form-login/>-->
<!--更换自己的登陆页面
login-page:设定登陆login.html页面
default-target-url:默认登录成功后跳转的url
authentication-failure-forward-url:登陆失败后跳转的页面
-->
<security:form-login login-page="/login.html"
default-target-url="/index.html"
authentication-failure-url="/login.html"
/>
<security:csrf disabled="true"/>
</security:http>
2.在webapp下添加登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript">
</script>
</head>
<body>
<h1>自定义登陆页面</h1>
<form action="/login" method="post">
用户姓名:<input type="text" name="username"/><br/>
用户密码:<input type="password" name="password"/><br/>
<input type="submit" value="登陆"/>
</form>
</body>
</html>
3.启动服务,运行测试
遇到问题后,如何在登陆失败后像SpringSecurity原带登陆页面那样在页面上显示出对应的错误信息呢?
四、在登录页面显示错误信息
1.改springsecurity.xml配置文件
添加上述内容后,当再次访问页面,如果用户名或密码输入错误,则会重新跳回到登陆页面,并且会在浏览器 上把error=true显示在浏览器中,如下图所示:
在页面加载完后,获取浏览器URL后面的error属性,判断如果值为true,则说明登陆失败,发送请求从后台获取登 陆错误信息,修改代码如下:
2.在后台提供查询错误信息服务
编写Controller类,用来提供前端页面对错误信息查询的服务
@RestController
@RequestMapping("/login")
public class LoginController {
@RequestMapping("/getErrorMsg")
public Map getErrorMsg(HttpSession session){
Map map = new HashMap();
Exception e =(Exception)session.getAttribute("SPRING_SECURITY_LAST_EXCEPTION");
map.put("error",e.getMessage());
return map;
}
}
3.启动服务,运行测试
虽然错误信息能在页面上进行展示错误信息,但是错误信息为英文,如何将错误信息转换成中文呢?
五、自定义用户名不存在的错误信息
1.更改错误信息
在UserService中,当用户名不存在,可以通过抛出异常的方式来更改错误信息,修改代码如下:
public class UserService implements UserDetailsService {
public static Map<String, TbUser> users = new HashMap<String, TbUser>();
static {
users.put("root", new TbUser(1, "root", "root", "ROOT"));
users.put("admin", new TbUser(1, "admin", "admin", "ADMIN"));
}
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
if (users.containsKey(username)) {
//查询到用户信息
TbUser tbUser = users.get(username);
List<GrantedAuthority> list = new ArrayList<GrantedAuthority>();
list.add(new SimpleGrantedAuthority(tbUser.getRole()));
return new User(username, tbUser.getPassword(), list);
} else {
//未查询到用户信息
//return null;对比上述代码,修改内容如下
throw new UsernameNotFoundException("用户名不存在");
}
}
}
2.启动服务,运行测试
重新启动服务器,测试后会发现页面显示的错误信息更改为如下内容:
3.分析问题及解决方案
为什么没有显示"用户名不存在"的错误信息呢?经过读取源码可发现如下内容,DaoAuthenticationProvider类中有如下代码:
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//会调用自定义UserService中提供的loadUserByUsername方法,当该方法抛出异常后会被捕获到
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
当上述方法出现异常后,会抛给父类进行捕获
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
//此处hideUserNotFoundException为true,所以当出现UsernameNotFoundException会被封装成 Bad Credentials信息
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
//....
return createSuccessAuthentication(principalToReturn, authentication, user);
}
上述父类的代码中的hideUserNotFoundExceptions默认为true,所以UsernameNotFoundException会被封装成BadCredentialsException异常信息,所以需要修改hideUserNotFoundExceptions的值,通过以下配置文件进行设置,springsecurity.xml修改内容如下:
启动运行测试结果如下:
但是当用户输入的密码错误依然是"Bad credentials",那该如何解决?
六、自定义用户密码错误的错误信息
1.更改国际化文件
在springsecurity的核心jar包中有如下内容:
默认情况下springsecurity会从messages.properties中取对应的错误信息,在当前包下,会有一个对应的 messages_zh_CN.properties中有对应的中文错误信息,该如何更改默认获取错误的文件?
课有采用如下方式进行修改:
修改完后,启动测试,效果如下:
2.更改显示内容
上面已经能将错误信息显示成"坏的凭证"用户看着不是特别明白,所以可以通过自定义的方式进行错误信息展示,在自己项目中的resources目录下新建一个 messages目录,在messages目录下创建一个名字为messages_zh_CN.properties的配置文件,在配置文件中添加如下内容:
并将springsecurity.xml配置文件中将获取错误信息的配置文件路径更改成如下内容:
3.启动服务,运行测试
至此,对应的错误信息就已经能够按照我们的意愿进行显示。
七、总结
通过上述方式即可将错误信息进行自定义展示,当前springsecurity默认的错误信息还有非常多,大家可以根据自己 的需要将错误信息进行自定义展示。下面提供一个springsecurity.xml的完整配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd">
<!--配置放行资源 -->
<security:http pattern="/login.html" security="none"/>
<security:http pattern="/js/**" security="none"/>
<security:http pattern="/login/getErrorMsg.do" security="none"/>
<!--配置拦截规则 -->
<security:http use-expressions="false">
<security:intercept-url pattern="/**" access="ROLE_ADMIN"/>
<!--采用默认springSecurity提供的默认登陆页面 -->
<!--<security:form-login/>-->
<!--更换自己的登陆页面
login-page:设定登陆login.html页面
default-target-url:默认登录成功后跳转的url
authentication-failure-forward-url:登陆失败后跳转的页面
-->
<security:form-login login-page="/login.html"
login-processing-url="/login"
default-target-url="/index.html"
authentication-failure-url="/login.html?error=true"/>
<security:csrf disabled="true"/>
</security:http>
<security:authentication-manager>
<!--<security:authentication-provider user-service-ref="userDetailService">
</security:authentication-provider>-->
<security:authentication-provider ref="daoAuthenticationProvider">
</security:authentication-provider>
</security:authentication-manager>
<bean id="daoAuthenticationProvider"
class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="hideUserNotFoundExceptions" value="false"/>
<property name="userDetailsService" ref="userDetailService"/>
<property name="messageSource" ref="messageSource"/>
</bean>
<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames" value="classpath:messages/messages_zh_CN"/>
</bean>
<bean id="userDetailService" class="UserDetailService实现类"></bean>
</beans>