SpringMVC + security模塊 框架整合詳解
最近整理一個二手後台管理項目,整體大體可分為 : Springmvc + mybatis + tiles + easyui + security 這幾個模塊,再用maven做管理。剛拿到手裡面東西實在太多,所以老大讓重新整一個,只挑出骨架。不可否認,架構整體規劃得還是很好的,尤其是前端界面用tiles+easyui ,開發很高效!其實,之前雖然聽說過security,但做過的項目都沒用過security,所以也算是一個新手!整合過程還是很燒腦的。
1、首先是springmvc部分,也是框架的最核心骨架部分。springmvc也是基於servlet框架完成的,所以我們都可以從web.xml入手,然後順藤摸瓜,整理出整個架構。web.xml中主要部分是放置spring的各種配置:
- <context-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>
- /WEB-INF/spring/appServlet/servlet-context.xml
- </param-value>
- </context-param>
和
- <servlet>
- <servlet-name>appServlet</servlet-name>
- <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
- <init-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
- </init-param>
- <load-on-startup>1</load-on-startup>
- <async-supported>true</async-supported>
- </servlet>
- <servlet-mapping>
- <servlet-name>appServlet</servlet-name>
- <url-pattern>/</url-pattern>
- </servlet-mapping>
這兩部分指定了配置文件地址和web請求地址攔截規則。需要注意的是,然後配置文件里就是常用的註解、數據源、之類的,不一而足。
2、然後是security,本文實現了自定義的用戶信息登錄認證,主要部分:spring的security主要注意幾個地方:
1)web.xml中配置security必須的過濾器,注意名字必須是springSecurityFilterChain,這是內置的過濾器;然後將過濾規則設置為/*,表示對所有請求都攔截。
2) 關於security的配置文件必須在<context-param>中加入,因為ContextLoaderListener在載入的時候就會去載入security相關的東西,而此時springmvc(servlet)模塊還沒有初始化。
二者配置如下:
- <context-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>
- /WEB-INF/spring/appServlet/servlet-context.xml
- </param-value>
- </context-param>
- <!-- Creates the Spring Container shared by all Servlets and Filters -->
- <listener>
- <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
- </listener>
- <!-- 用戶許可權模塊 -->
- <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>
本文中security配置文件再servlet-context.xml中被引入,形如:
<beans:import resource="spring-security.xml"/>
3)spring-security.xml配置文件:
現在來看這個xml文件:
首先 <http>標籤部分聲明了訪問攔截規則,這是security的關鍵。在這部分,我們先聲明了一些不需要驗證的資源和訪問路徑;然後是地址攔截規則部分:該部分攔截路徑是/**,**表示可以跨目錄結構,因此此處攔截該站點所有請求(除之前聲明security=「none」的以外),然後攔截規則access="isAuthenticated()",這是SecurityExpressionRoot中的判斷是否認證過的方法之一,跟hasRole()類似,官方描述是Returns true if the user is not anonymous ,也就是用戶認證後返回true。
- <?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"
- xmlns:security="http://www.springframework.org/schema/security"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
- http://www.springframework.org/schema/security
- http://www.springframework.org/schema/security/spring-security-3.2.xsd">
- <!-- For Web security -->
- <http pattern="/js/**" security="none"/>
- <http pattern="/images/**" security="none"/>
- <http pattern="/mgmt/**" security="none"/>
- <http pattern="/image_sys/**" security="none"/>
- <http pattern="/style/**" security="none"/>
- <http pattern="/view/login.jsp" security="none"/>
- <http pattern="/view/login/forgotPassword.jsp" security="none"/>
- <http pattern="/securityCodeImage.html" security="none"/>
- <http pattern="/checkSecurityCode.html" security="none"/>
- <http pattern="/admin/validateMobile.html" security="none"/>
- <http pattern="/verifycode/sendVerifyCode.html" security="none"/>
- <http pattern="/admin/resetPassword.html" security="none"/>
- <http use-expressions="true" entry-point-ref="authenticationProcessingFilterEntryPoint" access-denied-page="/view/403.jsp">
- <intercept-url pattern="/**" access="isAuthenticated()" />
- <remember-me />
- <!-- <expression-handler ref="webSecurityExpressionHandler"/> -->
- <custom-filter ref="loginFilter" position="FORM_LOGIN_FILTER" />
- <logout logout-url="/j_spring_security_logout" logout-success-url="/view/login.jsp" delete-cookies="JSESSIONID"/>
- </http>
- <!-- 未登錄的切入點 -->
- <beans:bean id="authenticationProcessingFilterEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
- <beans:property name="loginFormUrl" value="/view/login.jsp"></beans:property>
- </beans:bean>
- <!-- 登錄體系loginFilter -->
- <beans:bean id="loginFilter"
- class="com.security.AdminUsernamePasswordAuthenticationFilter">
- <beans:property name="filterProcessesUrl" value="/j_spring_security_check"></beans:property>
- <beans:property name="authenticationSuccessHandler" ref="myAuthenticationSuccessHandler"></beans:property>
- <beans:property name="authenticationFailureHandler" ref="myAuthenticationFailureHandler"></beans:property>
- <beans:property name="authenticationManager" ref="authenticationManager"></beans:property>
- </beans:bean>
- <!-- 驗證失敗顯示頁面 -->
- <beans:bean id="myAuthenticationFailureHandler"
- class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
- <beans:property name="defaultFailureUrl" value="/view/login.jsp?error=true" />
- </beans:bean>
- <!-- 驗證成功默認顯示頁面 -->
- <beans:bean id="myAuthenticationSuccessHandler"
- class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
- <beans:property name="alwaysUseDefaultTargetUrl" value="true" />
- <!--此處可以請求登錄首頁的action地址 -->
- <beans:property name="defaultTargetUrl" value="/index/homepage" />
- </beans:bean>
- <!-- authentication體系 -->
- <authentication-manager alias="authenticationManager" erase-credentials="false">
- <authentication-provider ref="authenticationProvider" />
- </authentication-manager>
- <beans:bean id="authenticationProvider"
- class="com.security.AdminAuthenticationProvider">
- <beans:property name="userDetailsService" ref="userDetailsService" />
- </beans:bean>
- <beans:bean id="userDetailsService"
- class="com.security.AdminUserDetailsService">
- <beans:property name="adminService" ref="adminServiceImpl"></beans:property>
- </beans:bean>
</beans:beans>
然後聲明了自定義的用戶登錄的攔截器loginFilter,用於在登錄時實現security認證。spring的登錄認證模塊執行順序為:
1、UsernamePasswordAuthenticationFilter的attemptAuthentication()方法,此部分可以做用戶名密碼的判斷,正確後繼續執行;
2、接下來調用AuthenticationManager的authenticate()方法(中途具體怎麼調用此處不深究),一個Mannager對應了多個authenticationProvider,其實最終是通過調用provider的authenticate()方法來進行認證的,只要provider的supports方法返回true即可聲明該provider或可進行認證,最後將被manager調用。在authenticate()方法中,調用UserDetails的loadUserByUserName()方法來載入登錄用戶信息,包括許可權信息,最後封裝成AbstractAuthenticationToken,這個對象就是security模塊認證後的用戶完整信息。
自定義認證主要類如下:
- /**
- * 用戶登錄驗證步驟一:attemptAutentication()
- */
- public class AdminUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
- public static final String VALIDATE_CODE = "validateCode";
- public static final String USERNAME = "j_username";
- public static final String PASSWORD = "j_password";
- public static final String EMPLOYEENO = "j_employeeNo";
- @Resource
- private AdminService adminService;
- @Override
- public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
- throws AuthenticationException {
- if (!request.getMethod().equals("POST")) {
- throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
- }
- // 判斷用戶信息
- // UsernamePasswordAuthenticationToken實現 Authentication
- UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
- adminModel.getAdminId(), password);
- // 允許子類設置詳細屬性
- setDetails(request, authRequest);
- // 運行UserDetailsService的loadUserByUsername 再次封裝Authentication
- return this.getAuthenticationManager().authenticate(authRequest);
- }
- @Override
- protected String obtainUsername(HttpServletRequest request) {
- Object obj = request.getParameter(USERNAME);
- return null == obj ? "" : obj.toString();
- }
- @Override
- protected String obtainPassword(HttpServletRequest request) {
- Object obj = request.getParameter(PASSWORD);
- return null == obj ? "" : obj.toString();
- }
- }
- /**
- * <span stylex="font-family: Arial, Helvetica, sans-serif;">用戶登錄驗證步驟二:</span><span stylex="font-family: Arial, Helvetica, sans-serif;">authenticate</span><span stylex="font-family: Arial, Helvetica, sans-serif;">()</span><span stylex="font-family: Arial, Helvetica, sans-serif;">
- </span> */
- public class AdminAuthenticationProvider implements AuthenticationProvider {
- protected Logger logger = LoggerFactory.getLogger(this.getClass());
- private UserDetailsService userDetailsService = null;
- public AdminAuthenticationProvider() {
- super();
- }
- /**
- * @param userDetailsService
- */
- public AdminAuthenticationProvider(UserDetailsService userDetailsService) {
- super();
- this.userDetailsService = userDetailsService;
- }
- public UserDetailsService getUserDetailsService() {
- return userDetailsService;
- }
- public void setUserDetailsService(UserDetailsService userDetailsService) {
- this.userDetailsService = userDetailsService;
- }
- /**
- * provider的authenticate()方法,用於登錄驗證
- */
- @SuppressWarnings("unchecked")
- public Authentication authenticate(Authentication authentication) throws AuthenticationException {
- // 1. Check username and password
- try {
- doLogin(authentication);
- } catch (Exception e) {
- if (e instanceof AuthenticationException) {
- throw (AuthenticationException) e;
- }
- logger.error("failure to doLogin", e);
- }
- // 2. Get UserDetails
- UserDetails userDetails = null;
- try {
- userDetails = this.userDetailsService.loadUserByUsername(authentication.getName());
- } catch (Exception e) {
- if (e instanceof AuthenticationException) {
- throw (AuthenticationException) e;
- }
- logger.error("failure to get user detail", e);
- }
- // 3. Check and get all of admin roles and contexts.
- Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) userDetails.getAuthorities();
- if (authorities != null && !authorities.isEmpty()) {
- AdminAuthenticationToken token = new AdminAuthenticationToken(authentication.getName(),
- authentication.getCredentials(), authorities);
- token.setDetails(userDetails);
- return token;
- }
- throw new BadCredentialsException("沒有分配許可權");
- }
- protected void doLogin(Authentication authentication) throws AuthenticationException {
- }
- @Override
- public boolean supports(Class<?> authentication) {
- // TODO Auto-generated method stub
- return true;
- }
- }
- /**用戶詳細信息獲取
- */
- public class AdminUserDetailsService implements UserDetailsService {
- private Logger logger = LoggerFactory.getLogger(AdminUserDetailsService.class);
- private AdminService adminService;
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- AdminUserDetails userDetails = new AdminUserDetails();
- userDetails.setAdminId(username);
- ///載入用戶基本信息
- AdminModel adminModel = adminService.getAdminByAdminId(username);
- try {
- PropertyUtils.copyProperties(userDetails, adminModel);
- } catch (IllegalAccessException e) {
- logger.error("用戶信息複製到userDetails出錯",e);
- } catch (InvocationTargetException e) {
- logger.error("用戶信息複製到userDetails出錯",e);
- } catch (NoSuchMethodException e) {
- logger.error("用戶信息複製到userDetails出錯",e);
- }
- //載入許可權信息
- List<AdminRoleGrantedAuthority> authorities = this.adminService.getAuthorityByUserId(username);
- if (authorities == null || authorities.size() == 0) {////如果為普通用戶
- if (isCommonUserRequest()) {
- AdminRoleGrantedAuthority authority =
- new AdminRoleGrantedAuthority(AdminRoleGrantedAuthority.ADMIN_ROLE_TYPE_COMMON_USER);
- userDetails.getAuthorities().add(authority);
- } else {
- logger.warn("person authorities is empty, personId is [{}]", username);
- }
- }
- //載入用戶許可權
- userDetails.getAuthorities().addAll(authorities);
- ///這個就是許可權系統最後的用戶信息
- return userDetails;
- }
- private boolean isCommonUserRequest() {
- // TODO Auto-generated method stub
- return true;
- }
- public AdminService getAdminService() {
- return adminService;
- }
- public void setAdminService(AdminService adminService) {
- this.adminService = adminService;
- }
- }
- public class AdminAuthenticationToken extends AbstractAuthenticationToken {
- /**
- *
- */
- private static final long serialVersionUID = 5976309306377973996L;
- private final Object principal;
- private Object credentials;
- public AdminAuthenticationToken(Object principal, Object credentials) {
- super(null);
- this.principal = principal;
- this.credentials = credentials;
- setAuthenticated(false);
- }
- c AdminAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
- super(authorities);
- this.principal = principal;
- this.credentials = credentials;
- super.setAuthenticated(true); //注意,這裡設置為true了! must use super, as we override
- }
- public Object getCredentials() {
- return this.credentials;
- }
- 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");
- }
- super.setAuthenticated(false);
- }
- @Override
- public void eraseCredentials() {
- super.eraseCredentials();
- credentials = null;
- }
- }
到這裡基本就完成了spring security的設置,
4)最後就是jsp頁面部分,頁面主要是表單的action地址必須為:path+/j_spring_security_check,其中path是項目路徑,然後登錄的時候就可以使用security模塊了!
至於文章中可能涉及到的一些bean或者service,這裡就不貼代碼了,畢竟項目較為完善,涉及東西太多,另外每個項目業務邏輯也不一樣。
在整個框架整合過程中,遇到了很多奇怪的問題,很多是因為版本不一致引起的,建議用maven進行jar等依賴包的統一管理。
※我為何一直強調外包公司別去
※.gitignore詳解及編寫
TAG:程序員小新人學習 |