spring security学习笔记

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!”);
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值