Spring security
spring security主要包括了几个文件和jar包,
主要配置了security的专属配置文件spring-security.xml,
以及表单提交时,注意
用户名输入框name为 j_username,
密码输入框name为j_password,
action为:j_spring_security_check
数据库User最好将账号字段设置为username,密码字段设置为password,另外还有一个账号有效性字段enabled,1为有效,0为无效,这三个字段是基本属性,
另外还要有一个权限表,里面要有username,role字段,假如没有这个表,当然也可以在配置文件里面用sql查询
select xxx as username, yyy as role form xxxx where username = xxxx
一个角色对应一个用户,准确来说这个应该叫用户权限表,不叫角色表;
以上不包含登陆次数限制,登陆次数限制的话还有其他3个字段。
另外建立一个登录失败次数表,字段自己看着整,一般有个username,有个登陆次数,每次失败+1,有个最后登录失败时间。
其他在代码中写了详细说明,这就不一一解释了。
XML配置文件(基础版,不带限制次数)
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
">
<!-- auto-config = true 则使用from-login. 如果不使用该属性 则默认为http-basic(没有session).
access-denied-page:出错后跳转到的错误页面; -->
<!-- 代表所有的访问协议是http auto-config="true",security有7个过滤器,这个属性就是自动配置过滤器的顺序,如果没有这个,
就会有顺序出错的异常可能产生; -->
<!-- 配置保护资源 -->
<http auto-config="true" use-expressions="true">
<!-- intercept-url 表示拦截的URL,就是哪些URL是有访问权限限制的 access表示哪些角色能够访问这个页面 角色的设置,默认的是ROLE_
user(这个是角色) ROLE_ ADMIN filter="none",表示本页面不拦截,不过滤 method="GET"表示本页面过滤的方法
requires-channel="http"表示过滤的信道 注意;使用 springSecurity框架的Form表单提交去向都往这里提;至于登录成功的去向则
j_spring_security_check' 默认检验登陆界面
如果只要某个特定角色访问该页面,那么access="hasRole('Role名(数据库中的值)')
这里的1代表县级用户,2代表市级用户,3代表省级用户,参照了area表里面的deptLevel;
-->
<intercept-url pattern="/**" access="hasAnyRole('1','2','3')" />
<!-- 403错误访问页面(没有相应权限的错误)-->
<access-denied-handler error-page="/403" />
<!-- 默认登陆界面,注意登陆界面的表单映射 authentication-failure-url 认证失败到哪个页面 default-target-url
登陆成功到达哪个页面 -->
<!-- error,代表从handler传过去的错误信息,错误信息在login Action里面配置 -->
<form-login login-page="/login" default-target-url="/index.html"
authentication-failure-url="/login?error" />
<!-- logout,代表退出登录时给的提示信息 -->
<logout logout-success-url="/login?logout" />
</http>
<!-- 配置查询登陆用户的查询数据来源 ,这里是没有登陆次数限制的设置-->
<!-- 配置用户 -->
<authentication-manager>
<!-- 认证的提供者,提供认证人的信息 -->
<authentication-provider>
<!-- 通过数据库获得用户信息
users-by-username-query:查询验证需要的用户信息;
authorities-by-username-query:查询验证需要的用户权限信息
-->
<jdbc-user-service data-source-ref="dbcp_dateSource"
users-by-username-query="select username username,password password,enabled enabled from t_users where username=?"
authorities-by-username-query="select username username,role role from t_user_roles where username=?"
/>
<!-- 提供最简单的用户使用者,直接在配置文件中指定角色信息,authorities代表用户拥有的角色,多个角色用逗号隔开 -->
<!-- <user-service>
<user name="xiaodudu" password="xiaodudu" authorities="ROLE_ADMIN"/>
<user name="mike" password="mike" authorities="ROLE_manager"/>
</user-service> -->
</authentication-provider>
</authentication-manager>
</beans:beans>
用户登录次数限制的实现
新增一张表T_USER_ATTEMPTS,用来辅助记录每个用户登录错误时的尝试次数
id,
username,用户名,外检 引用t_users中的username
attempts,登录次数
为其建立一个model实体类,然后对应的dao有三个方法
数据库中用户表需要3条字段:
用户登录次数。
用户是否锁定。
用户最后登录失败的日期;
完成思路:在数据库用户字段添加一条属性,用户登录次数,然后使用该用户名登录失败后,会将数据库中该字段 数值+1;
==另外,根据security格式,需要在User数据表中添加三个字段:
D_ACCOUNTNONEXPIRED,NUMBER(1) – 表示帐号是否未过期
D_ACCOUNTNONLOCKED,NUMBER(1), – 表示帐号是否未锁定
D_CREDENTIALSNONEXPIRED,NUMBER(1) –表示登录凭据是否未过期==
基于JDBC的UserDetailsDao实现
private static final String SQL_USERS_COUNT = "SELECT COUNT(*) FROM t_users WHERE d_username = ?";
private static final int MAX_ATTEMPTS = 3;
//登录失败后增长一次登录次数
void updateFailAttempts(String username){
//得到用户登录次数
UserAttempts user = getUserAttempts(username);
//如果没有得到,并且存在username,那么新建一个登录次数记录
if (user == null) {
if (isUserExists(username)) {
// if no record, insert a new
getJdbcTemplate().update(SQL_USER_ATTEMPTS_INSERT,
new Object[] { username, 1, new Date() });
}
}
//如果得到登录次数,并且核对成功,在原有基础上登录次数+1,更改最后次登录错误时间
else {
if (isUserExists(username)) {
// update attempts count, +1
getJdbcTemplate().update(SQL_USER_ATTEMPTS_UPDATE_ATTEMPTS,
new Object[] { new Date(), username });
}
//如果超过最大登录次数,那么将用户锁定,并且抛出一个异常,用户被锁定异常
if (user.getAttempts() + 1 >= MAX_ATTEMPTS) {
// locked user
getJdbcTemplate().update(SQL_USERS_UPDATE_LOCKED,
new Object[] { false, username });
// throw exception
throw new LockedException("User Account is locked!");
}
}
}
}
//重置登录失败的次数
void resetFailAttempts(String username);
//得到登录失败次数,将登录次数设置为0,username设置为null
UserAttempts getUserAttempts(String username);
基于hibernate实现版本;
public class UserDetailsDaoImpl extends JdbcDaoSupport implements IUserDetailsDao {
private static final String SQL_USERS_COUNT = "SELECT COUNT(*) FROM t_users WHERE d_username = ?";
private static final int MAX_ATTEMPTS = 3;
@Resource
private SessionFactory sf;
@Override
public void updateFailAttempts(String username) {
Session session = sf.getCurrentSession();
UserAttempts user = getUserAttempts(username);
if (user == null) {
if (isUserExists(username)) {
// if no record, insert a new
session.save(new UserAttempts(username,1,new Date()));
}
} else {
if (isUserExists(username)) {
// update attempts count, +1
user.setAttempts(user.getAttempts()+1);
session.update(user);
}
if (user.getAttempts() + 1 >= MAX_ATTEMPTS) {
// locked user
User u = getUser(username);
session.update(u);
// throw exception
throw new LockedException("User Account is locked!");
}
}
System.out.println("用户登录了" + user.getAttempts());
}
@Override
public void resetFailAttempts(String username) {
}
@Override
public UserAttempts getUserAttempts(String username) {
try {
Session session = sf.getCurrentSession();
UserAttempts userAttempts =
(UserAttempts) session.createQuery("select UserAttempts ua where us.username = ?")
.setParameter(0, username).uniqueResult();
return userAttempts;
} catch (EmptyResultDataAccessException e) {
return null;
}
}
private boolean isUserExists(String username) {
boolean result = false;
int count = getJdbcTemplate().queryForObject(SQL_USERS_COUNT,
new Object[] { username }, Integer.class);
if (count > 0) {
result = true;
}
return result;
}
private User getUser(String username){
Session session = sf.getCurrentSession();
return (User) session.createQuery("selecr from User u where u.username = ?").setParameter(0, username).uniqueResult();
}
}
建立一个UserDetailService
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.stereotype.Service;
/**
该类继承了spring-security提供的jdbcDao实现类,就是用户相关权限的查询。
*/
@Service("userDetailsService")
public class CustomUserDetailsService extends JdbcDaoImpl {
@Override
public void setUsersByUsernameQuery(String usersByUsernameQueryString) {
super.setUsersByUsernameQuery(usersByUsernameQueryString);
}
@Override
public void setAuthoritiesByUsernameQuery(String queryString) {
super.setAuthoritiesByUsernameQuery(queryString);
}
// 得到登录账号信息
@Override
public List<UserDetails> loadUsersByUsername(String username) {
return getJdbcTemplate().query(super.getUsersByUsernameQuery(),
new String[] { username }, new RowMapper<UserDetails>() {
public UserDetails mapRow(ResultSet rs, int rowNum)
throws SQLException {
String username = rs.getString("username");
String password = rs.getString("password");
boolean enabled = rs.getBoolean("enabled");
//得到账号是否过期
boolean accountNonExpired = rs
.getBoolean("accountNonExpired");
//得到账号登录凭据是否未过期
boolean credentialsNonExpired = rs
.getBoolean("credentialsNonExpired");
//得到账号是否锁定情况
boolean accountNonLocked = rs
.getBoolean("accountNonLocked");
return new User(username, password, enabled,
accountNonExpired, credentialsNonExpired,
accountNonLocked, AuthorityUtils.NO_AUTHORITIES);
}
});
}
/*创建一个spring-security框架注入了用户权限信息的对象UserDetails
List<GrantedAuthority> combinedAuthorities,表示用户所具有的权限对象集合
该UserDetails是从验证Handler传过来的对象;
*/
@Override
public UserDetails createUserDetails(String username,
UserDetails userFromUserQuery,
List<GrantedAuthority> combinedAuthorities) {
String returnUsername = userFromUserQuery.getUsername();
if (super.isUsernameBasedPrimaryKey()) {
returnUsername = username;
}
//返回一个包含了权限等信息的用户对象
return new User(returnUsername, userFromUserQuery.getPassword(),
userFromUserQuery.isEnabled(),
userFromUserQuery.isAccountNonExpired(),
userFromUserQuery.isCredentialsNonExpired(),
userFromUserQuery.isAccountNonLocked(), combinedAuthorities);
}
}
CustomUserDetailsService
自己写的用户数据库信息提供给验证器验证的数据提供类
package com.cnblogs.yjmyzz.provider;
import java.util.Date;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import com.cnblogs.yjmyzz.dao.UserDetailsDao;
import com.cnblogs.yjmyzz.model.UserAttempts;
@Component("authenticationProvider")
public class LimitLoginAuthenticationProvider extends DaoAuthenticationProvider {
//对userDetail的操作(登录次数限制)
UserDetailsDao userDetailsDao;
/**
返回一个权限登录
*/
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
try {
Authentication auth = super.authenticate(authentication);
// 如果到达了这里,说明登录已经成功,否则的话会抛出异常
// 重置一个账户的登录次数登录次数
//authentication.getName(),得到登录账户的用户名
userDetailsDao.resetFailAttempts(authentication.getName());
//返回用户具有的权限对象
return auth;
} catch (BadCredentialsException e) {
//如果发生了错误,没有读取到权限信息,那么登录失败次数+1
userDetailsDao.updateFailAttempts(authentication.getName());
throw e;
} catch (LockedException e) {
//如果抓住账户已经锁定异常,并且还没有包含错误信息
String error = "";
//得到用户登录次数
UserAttempts userAttempts = userDetailsDao
.getUserAttempts(authentication.getName());
if (userAttempts != null) {
//得到用户登录的最后错误时间
Date lastAttempts = userAttempts.getLastModified();
//给出提示,账号被锁定,最后次登录事件
error = "User account is locked! <br><br>Username : "
+ authentication.getName() + "<br>Last Attempts : "
+ lastAttempts;
} else {
//将异常的错误信息放到error中
error = e.getMessage();
}
//重新抛出账号锁定异常
throw new LockedException(error);
}
}
public UserDetailsDao getUserDetailsDao() {
return userDetailsDao;
}
public void setUserDetailsDao(UserDetailsDao userDetailsDao) {
this.userDetailsDao = userDetailsDao;
}
}
对上面验证类的XML配置,当然一些bean的配置可以用注解来实现,比如userDetailDao
<!--在spring上注册一个dao,并且注入数据源(链接数据库)-->
<beans:bean id="userDetailsDao" class="com.wang.dao.UserDetailsDaoImpl">
<beans:property name="dataSource" ref="dbcp_dataSource" />
</beans:bean>
<beans:bean id="customUserDetailsService"
class="com.wang.service.CustomUserDetailsService">
<beans:property name="usersByUsernameQuery"
value="SELECT d_username username,d_password password, d_enabled enabled,d_accountnonexpired accountnonexpired,d_accountnonlocked accountnonlocked,d_credentialsnonexpired credentialsnonexpired FROM t_users WHERE d_username=?" />
<beans:property name="authoritiesByUsernameQuery"
value="SELECT d_username username, d_role role FROM t_user_roles WHERE d_username=?" />
<beans:property name="dataSource" ref="dbcp_dataSource" />
</beans:bean>
<!-- 注册一个权限提供者类 -->
<beans:bean id="authenticationProvider"
class="com.wang.provider.LimitLoginAuthenticationProvider">
<beans:property name="userDetailsService" ref="customUserDetailsService" />
<beans:property name="userDetailsDao" ref="userDetailsDao" />
</beans:bean>
<authentication-manager> <authentication-provider ref="authenticationProvider"
/> </authentication-manager>
自定义Login/LogoutFilter,AuthenticationProvider、AuthenticationToken(在两金项目中可以不用)
From_Login_Fiter,登录过滤器;http/form-login
Logout_filter,登出过滤器http/logout
security给定的默认登陆表单是Post提交方法,如果想改变,使用get提交,可以自己创建一个类,用来继承CustomLoginFilter extends UsernamePasswordAuthenticationFilter过滤器;
package com.cnblogs.yjmyzz;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
//import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter {
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
//如果请求方式不是Post,抛出错误;
// if (!request.getMethod().equals("POST")) {
// throw new AuthenticationServiceException(
// "Authentication method not supported: "
// + request.getMethod());
// }
String username = obtainUsername(request).toUpperCase().trim();
String password = obtainPassword(request);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
如果想再登陆成功后,加一点自己的处理逻辑
package com.cnblogs.yjmyzz;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
public class CustomLoginHandler extends
SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
super.onAuthenticationSuccess(request, response, authentication);
//这里可以追加开发人员自己的额外处理
System.out
.println("CustomLoginHandler.onAuthenticationSuccess() is called!");
}
}
如果还想在退出后加点自己的逻辑(比如注销后,清空额外的Cookie之类\记录退出时间、地点之类),可重写doFilter方法,自行定义logoutSuccessHandler,然后在运行时,通过构造函数注入即可。
package com.cnblogs.yjmyzz;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
public class CustomLogoutHandler implements LogoutHandler {
public CustomLogoutHandler() {
}
@Override
public void logout(HttpServletRequest request,
HttpServletResponse response, Authentication authentication) {
System.out.println("CustomLogoutSuccessHandler.logout() is called!");
}
}
自定义登录验证,例如验证账号密码外,还要验证验证码等,
为了能让这些额外添加的输入项,传递到Provider中参与验证,就需要对UsernamePasswordAuthenticationToken进行扩展,参考代码如下:
package com.cnblogs.yjmyzz;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
public class CustomAuthenticationToken extends
UsernamePasswordAuthenticationToken {
private static final long serialVersionUID = 5414106440823275021L;
public CustomAuthenticationToken(String principal, String credentials,
Integer questionId, String answer) {
super(principal, credentials);
this.answer = answer;
this.questionId = questionId;
}
private String answer;
private Integer questionId;
public String getAnswer() {
return answer;
}
public void setAnswer(String answer) {
this.answer = answer;
}
public Integer getQuestionId() {
return questionId;
}
public void setQuestionId(Integer questionId) {
this.questionId = questionId;
}
}
package com.cnblogs.yjmyzz;
import java.io.UnsupportedEncodingException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter {
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
//解决中文诗句的post乱码问题
try {
request.setCharacterEncoding("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// if (!request.getMethod().equals("POST")) {
// throw new AuthenticationServiceException(
// "Authentication method not supported: "
// + request.getMethod());
// }
String username = obtainUsername(request).toUpperCase().trim();
String password = obtainPassword(request);
//获取用户输入的下一句答案
String answer = obtainAnswer(request);
//获取问题Id(即: hashTable的key)
Integer questionId = obtainQuestionId(request);
//这里将原来的UsernamePasswordAuthenticationToken换成我们自定义的CustomAuthenticationToken
CustomAuthenticationToken authRequest = new CustomAuthenticationToken(
username, password, questionId, answer);
//这里就将token传到后续验证环节了
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainAnswer(HttpServletRequest request) {
return request.getParameter(answerParameter);
}
protected Integer obtainQuestionId(HttpServletRequest request) {
return Integer.parseInt(request.getParameter(questionIdParameter));
}
private String questionIdParameter = "questionId";
private String answerParameter = "answer";
public String getQuestionIdParameter() {
return questionIdParameter;
}
public void setQuestionIdParameter(String questionIdParameter) {
this.questionIdParameter = questionIdParameter;
}
public String getAnswerParameter() {
return answerParameter;
}
public void setAnswerParameter(String answerParameter) {
this.answerParameter = answerParameter;
}
}
现在,CustomAuthenticationProvider中的additionalAuthenticationChecks方法中,就能拿到用户提交的下一句答案,进行相关验证了:
“`@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
// 转换为自定义的token
CustomAuthenticationToken token = (CustomAuthenticationToken) authentication;
String poem = LoginQuestion.getQuestions().get(token.getQuestionId());
// 校验下一句的答案是否正确
if (!poem.split(“/”)[1].equals(token.getAnswer())) {
throw new BadAnswerException(“the answer is wrong!”);
}
}