(一) 数据库与实体类设计(mysql)
-- 权限 DROP TABLE IF EXISTS `me`.`tbl_permission` ; CREATE TABLE IF NOT EXISTS `me`.`tbl_permission` ( `_id` INT NOT NULL AUTO_INCREMENT , `_name` VARCHAR(45) NOT NULL , `_desc` VARCHAR(255) NULL , PRIMARY KEY (`_id`) , UNIQUE INDEX `_name_UNIQUE` (`_name` ASC) ) ENGINE = InnoDB; -- 角色 DROP TABLE IF EXISTS `me`.`tbl_role` ; CREATE TABLE IF NOT EXISTS `me`.`tbl_role` ( `_id` INT NOT NULL AUTO_INCREMENT , `_name` VARCHAR(45) NOT NULL , PRIMARY KEY (`_id`) , UNIQUE INDEX `_name_UNIQUE` (`_name` ASC) ) ENGINE = InnoDB; -- 角色权限关联表 DROP TABLE IF EXISTS `me`.`tbl_role_permission` ; CREATE TABLE IF NOT EXISTS `me`.`tbl_role_permission` ( `_id_permission` INT NOT NULL , `_id_role` INT NOT NULL , PRIMARY KEY (`_id_permission`, `_id_role`) , INDEX `fk_permission_QENOQPN` (`_id_permission` ASC) , INDEX `fk_role_PQEMAQWENGHJ` (`_id_role` ASC) , CONSTRAINT `fk_permission_QENOQPN` FOREIGN KEY (`_id_permission` ) REFERENCES `me`.`tbl_permission` (`_id` ) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT `fk_role_PQEMAQWENGHJ` FOREIGN KEY (`_id_role` ) REFERENCES `me`.`tbl_role` (`_id` ) ON DELETE NO ACTION ON UPDATE NO ACTION) ENGINE = InnoDB; -- 用户表 DROP TABLE IF EXISTS `me`.`tbl_user` ; CREATE TABLE IF NOT EXISTS `me`.`tbl_user` ( `_id` INT NOT NULL AUTO_INCREMENT , `_username` VARCHAR(45) NOT NULL , `_password` CHAR(32) NULL , `_disabled` CHAR(1) NULL , PRIMARY KEY (`_id`) , UNIQUE INDEX `_username_UNIQUE` (`_username` ASC) , UNIQUE INDEX `_disabled_UNIQUE` (`_disabled` ASC) ) ENGINE = InnoDB; -- 用户Email表 跟SpringSecurity没有关系 DROP TABLE IF EXISTS `me`.`tbl_email` ; CREATE TABLE IF NOT EXISTS `me`.`tbl_email` ( `_user_id` INT NOT NULL , `_email` VARCHAR(70) NOT NULL , `_order` INT NOT NULL , PRIMARY KEY (`_user_id`, `_email`) , UNIQUE INDEX `_email_UNIQUE` (`_email` ASC) , INDEX `fk_user_id_QWZLAKUIG` (`_user_id` ASC) , CONSTRAINT `fk_user_id_QWZLAKUIG` FOREIGN KEY (`_user_id` ) REFERENCES `me`.`tbl_user` (`_id` ) ON DELETE NO ACTION ON UPDATE NO ACTION) ENGINE = InnoDB; -- 用户角色关联表 DROP TABLE IF EXISTS `me`.`tbl_user_role` ; CREATE TABLE IF NOT EXISTS `me`.`tbl_user_role` ( `_user_id` INT NOT NULL , `_role_id` INT NOT NULL , PRIMARY KEY (`_user_id`, `_role_id`) , INDEX `fk_user_id_POIUYHJNB` (`_user_id` ASC) , INDEX `fk_role_id_MNHTRFVD` (`_role_id` ASC) , CONSTRAINT `fk_user_id_POIUYHJNB` FOREIGN KEY (`_user_id` ) REFERENCES `me`.`tbl_user` (`_id` ) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT `fk_role_id_MNHTRFVD` FOREIGN KEY (`_role_id` ) REFERENCES `me`.`tbl_role` (`_id` ) ON DELETE NO ACTION ON UPDATE NO ACTION) ENGINE = InnoDB;
显然这种设计方式实际上比较常见的,用户与角色是多对多关系;角色与权限也是多对多关系。
下面是实体类(为了节约篇幅getter和setter以及ORM相关的元注释略去)
import java.io.Serializable; import java.util.Set; import org.springframework.security.core.GrantedAuthority; /** 权限类实现 GrantedAuthority接口 */ public class Permission implements Serializable, GrantedAuthority { private Integer id; private String name; private String description; private Set<Role> roles; public String getAuthority() { return getName(); } }
import java.io.Serializable; import java.util.HashSet; import java.util.Set; import org.springframework.security.core.GrantedAuthority; /** 角色类 */ public class Role implements Serializable { private Integer id; private String name; private Set<Permission> permissions = new HashSet<Permission>(); private Set<User> users = new HashSet<User>(); // 为了简便起见 ROLE 和 Permission都视为一种权限 public GrantedAuthority generateGrantedAuthority() { return new GrantedAuthority() { public String getAuthority() { return getName(); } }; } }
import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; /** 用户类实现UserDetails接口 */ public class User implements Serializable, UserDetails { private Integer id; private String username; private String password; private List<String> emails = new ArrayList<String>(); // 和SpringSecurity没有关系的业务字段 private String disabled; private Set<Role> roles = new HashSet<Role>(); public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> list = new ArrayList<GrantedAuthority>(); for (Role role : roles) { list.add(role.generateGrantedAuthority()); for (Permission permission : role.getPermissions()) { list.add(permission); } } // 排序其实没有必要 // Collections.sort(list, GrantedAuthorityComparators.REVERSE); return list; } public String getPassword() { return password; } public String getUsername() { return username; } public boolean isAccountNonExpired() { return true; } public boolean isAccountNonLocked() { return true; } public boolean isCredentialsNonExpired() { return true; } public boolean isEnabled() { return disabled.equalsIgnoreCase("F"); } } class GrantedAuthorityComparators implements Comparator<GrantedAuthority> { public static final Comparator<GrantedAuthority> DEFAULT = new GrantedAuthorityComparators(); public static final Comparator<GrantedAuthority> REVERSE = new Comparator<GrantedAuthority>() { public int compare(GrantedAuthority o1, GrantedAuthority o2) { return - DEFAULT.compare(o1, o2); } }; private GrantedAuthorityComparators() { super(); } public int compare(GrantedAuthority g1, GrantedAuthority g2) { return g1.getAuthority().compareTo(g2.getAuthority()); } }
(二) 编写重要的UserDetailsService实现类
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.yingzhuo.me.domain.User; @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) public interface UserDao extends UserDetailsService { public User findUserByUsernameAndPassword(String username, String password); }
import org.hibernate.Query; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Repository; @Repository("userDao") public class UserDaoImpl extends BaseDao implements UserDao { public User findUserByUsernameAndPassword(String username, String password) { final String hql = "from User as u left join fetch u.roles where u.username = :username and u.password = :password"; Query query = getSession().createQuery(hql) .setParameter("username", username) .setParameter("password", password); return (User) query.uniqueResult(); } public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { final String hql = "from User as u left join fetch u.roles where u.username = :username"; Query query = getSession().createQuery(hql).setParameter("username", username); User user = (User) query.uniqueResult(); if (user == null) { throw new UsernameNotFoundException("Username '" + username + "' not found"); } // 取消延迟加载 for (Role role : user.getRoles()) { for (Permission per : role.getPermissions()) { per.getName(); } } return user; } }
虽然SpringSecurity框架提供的接口很多,真正要亲自实现的不多三个而已
- org.springframework.security.core.GrantedAuthority
- org.springframework.security.core.userdetails.UserDetails
- org.springframework.security.core.userdetails.UserDetailsService
(三) 编写Spring Security 配置文件
尽管SpringSecurity提供了<http>元素来简化配置,简化过后有相当多的细节被隐藏起来了。
有时候想改变一下框架的默认行为十分不便。我还是用传统的Bean方式。
这样虽然麻烦,但是掌握之后,一旦看哪个bean不顺眼就可以方便的取而代之。强大灵活!
3.0 xml的schema, 因为是用bean的方式配置嘛,默认命名空间当然是用beans方便。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c" xmlns:util="http://www.springframework.org/schema/util" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:security="http://www.springframework.org/schema/security" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd"> </beans>
3.1 web项目部署描述
<!-- Spring Security 核心拦截器组 --> <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> <!-- Web Application Context --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:spring-bean.xml classpath:spring-orm.xml classpath:spring-security.xml </param-value> </context-param> <!-- 支持SpringSecurity Session并发控制 --> <listener> <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class> </listener>
提醒一下,如果SpringSecurity和Struts2共同使用的话 org.springframework.web.filter.DelegatingFilterProxy 一定要配置在
org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter前面。要不然你的SpringSecurity根本不会起任何作用的。
当然如果使用的是 SpringMVC框架的话那根本无所谓配置顺序,因为SpringMVC的核心转发器是一个Servlet的实现。
3.2 配置SpringSecurity过滤器组 (我一般就用这九个) 顺序很重要
<bean id="springSecurityFilterChain" class="org.springframework.security.web.FilterChainProxy"> <security:filter-chain-map request-matcher="ant" > <security:filter-chain pattern="/**" filters=" channelProcessingFilter, concurrencyFilter, securityContextPersistenceFilter, logoutFilter, usernamePasswordProcessingFilter, rememberMeProcessingFilter, anonymousProcessingFilter, exceptionTranslationFilter, filterSecurityInterceptor" /> </security:filter-chain-map> </bean>
3.3 channelProcessingFilter: 常用来将某些HTTP协议的URL重定向到HTTPS协议
<bean id="channelProcessingFilter" class="org.springframework.security.web.access.channel.ChannelProcessingFilter"> <property name="channelDecisionManager" ref="channelDecisionManager" /> <property name="securityMetadataSource"> <security:filter-security-metadata-source request-matcher="ant"> <!-- <security:intercept-url pattern="/just/test" access="REQUIRES_SECURE_CHANNEL" /> --> <security:intercept-url pattern="/**" access="ANY_CHANNEL" /> </security:filter-security-metadata-source> </property> </bean> <bean id="channelDecisionManager" class="org.springframework.security.web.access.channel.ChannelDecisionManagerImpl"> <property name="channelProcessors"> <list> <ref local="secureChannelProcessor" /> <ref local="insecureChannelProcessor" /> </list> </property> </bean> <bean id="secureChannelProcessor" class="org.springframework.security.web.access.channel.SecureChannelProcessor" /> <bean id="insecureChannelProcessor" class="org.springframework.security.web.access.channel.InsecureChannelProcessor" />
3.4 concurrencyFilter:HttpSession并发过滤器
<bean id="concurrencyFilter" class="org.springframework.security.web.session.ConcurrentSessionFilter"> <property name="sessionRegistry" ref="sessionRegistry" /> <property name="expiredUrl" value="/common/session-expired" /> <!-- 配置Session过期后重定向的地址 --> </bean> <bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" /> <bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy"> <constructor-arg index="0" ref="sessionRegistry" /> <property name="maximumSessions" value="1" /> </bean>
3.5 securityContextPersistenceFilter:获取或存储一个SecurityContext
<bean id="securityContextPersistenceFilter" class="org.springframework.security.web.context.SecurityContextPersistenceFilter" />
3.6 logoutFilter:监控一个实现退出功能的URL
<bean id="logoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter"> <constructor-arg index="0" value="/common/login" /> <!-- 退出后重定向 --> <constructor-arg index="1"> <array> <ref local="logoutHandler" /> <ref local="rememberMeServices" /> </array> </constructor-arg> <property name="filterProcessesUrl" value="/common/logout"/> <!-- 监控的URL --> </bean> <!-- 这个Bean注入到logoutFilter中去,它实际负责最后的扫尾工作,如把HttpSession实例删除 --> <bean id="logoutHandler" class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"> <property name="invalidateHttpSession" value="true" /> </bean>
这个bean没有没有默认构造方法。
3.7 usernamePasswordProcessingFilter:处理用户登录请求
<bean id="usernamePasswordProcessingFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter"> <property name="filterProcessesUrl" value="/common/login-process"/> <property name="usernameParameter" value="username"/> <property name="passwordParameter" value="password"/> <property name="authenticationManager" ref="customAuthenticationManager"/> <property name="rememberMeServices" ref="rememberMeServices"/> <property name="authenticationFailureHandler" ref="authenticationFailureHandler"/> <property name="sessionAuthenticationStrategy" ref="sas" /> </bean> <!-- 这个Bean注入到usernamePasswordProcessingFilter中去,他决定用户名和密码验证失败之后的动作 注意: 应设置行为为转发方式,否则保存在HttpServletRequest实例中的错误信息会因为重定向而丢失。 --> <bean id="authenticationFailureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"> <property name="defaultFailureUrl" value="/common/login"/> <property name="useForward" value="true" /> </bean>
3.8 rememberMeProcessingFilter: 实现"记住我"功能
<bean id="rememberMeProcessingFilter" class="org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter"> <property name="rememberMeServices" ref="rememberMeServices"/> <property name="authenticationManager" ref="customAuthenticationManager" /> </bean> <bean id="rememberMeServices" class="org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices"> <property name="key" value="#{securityKeys['remember-me']}" /> <!-- KEY 用于加密 两个一定要相同 --> <property name="parameter" value="_remember_me" /> <property name="tokenValiditySeconds" value="7200" /> <property name="tokenRepository" ref="inMemoryTokenRepository" /> <!-- 下面就是我自己实现的UserDetailsService 我给了alias "hibernateUserDetailsService" --> <property name="userDetailsService" ref="hibernateUserDetailsService" /> </bean> <bean id="rememberMeAuthenticationProvider" class="org.springframework.security.authentication.RememberMeAuthenticationProvider"> <property name="key" value="#{securityKeys['remember-me']}" /> <!-- KEY 用于加密 两个一定要相同 --> </bean> <bean id="inMemoryTokenRepository" class="org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl" /> <!-- <bean id="jdbcRememberMeTokenRepository" class="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl"> <property name="dataSource" ref="dataSource" /> </bean> -->
被我注释掉的jdbcRememberMeTokenRepository需要一个数据库表
这个表用来持久化RememberMeToken,生产环境一般还是要这样做的。
create table persistent_logins ( username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null );
3.9 anonymousProcessingFilter:如果用户不能通过验证则给添加一个匿名用户的角色
<bean id="anonymousProcessingFilter" class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter"> <property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/> <property name="key" value="#{securityKeys['anonymous']}"/> </bean>
3.10 exceptionTranslationFilter: 验证通不过?没有访问权限?这个Filter决定如果出现异常了到底应该这么办。
<bean id="exceptionTranslationFilter" class="org.springframework.security.web.access.ExceptionTranslationFilter"> <property name="authenticationEntryPoint" ref="authenticationEntryPoint" /> <property name="accessDeniedHandler" ref="accessDeniedHandler" /> </bean> <bean id="authenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint"> <property name="useForward" value="false" /> <property name="loginFormUrl" value="/common/login" /> </bean> <bean id="accessDeniedHandler" class="org.springframework.security.web.access.AccessDeniedHandlerImpl"> <property name="errorPage" value="/common/error"/> </bean>
3.11 filterSecurityInterceptor:核心过滤器的最后一个。它完成最终的授权判断
下面的配置有点多,那是因为filterSecurityInterceptor是个懒家伙。
它把工作委托AuthenticationManager接口,AuthenticationManager接口也不真的干活,
它委托多个AuthenticationProvider接口,当然其中一个AuthenticationProvider还是要
把工作委托给我们的UserDetailsService实现的。最后投票决定到最终结果。
<bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> <property name="authenticationManager" ref="customAuthenticationManager" /> <property name="accessDecisionManager" ref="affirmativeBased" /> <property name="securityMetadataSource"> <security:filter-security-metadata-source use-expressions="true"> <security:intercept-url pattern="/common/login" access="permitAll" /> <security:intercept-url pattern="/**" access="hasRole('ROLE_ADMIN')" /> </security:filter-security-metadata-source> </property> </bean> <bean id="customAuthenticationManager" class="org.springframework.security.authentication.ProviderManager"> <property name="authenticationEventPublisher" ref="defaultAuthEventPublisher"/> <property name="providers"> <list> <ref local="daoAuthenticationProvider"/> <ref local="anonymousAuthenticationProvider"/> <ref local="rememberMeAuthenticationProvider"/> </list> </property> </bean> <!-- 这个Bean决定了投票策略,decisionVoters只要有任意一个决定通过,那么结果就是通过。 --> <bean class="org.springframework.security.access.vote.AffirmativeBased" id="affirmativeBased"> <property name="decisionVoters"> <list> <ref bean="roleVoter"/> <ref bean="expressionVoter"/> <ref bean="authenticatedVoter"/> </list> </property> </bean> <bean class="org.springframework.security.access.vote.RoleVoter" id="roleVoter" /> <bean class="org.springframework.security.access.vote.AuthenticatedVoter" id="authenticatedVoter" /> <bean id="defaultAuthEventPublisher" class="org.springframework.security.authentication.DefaultAuthenticationEventPublisher"/> <bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> <property name="passwordEncoder" ref="md5PasswordEncoder"/> <property name="userDetailsService" ref="hibernateUserDetailsService" /> <!-- <property name="saltSource" ref="saltSource"/> --> </bean> <bean id="anonymousAuthenticationProvider" class="org.springframework.security.authentication.AnonymousAuthenticationProvider"> <property name="key" value="#{securityKeys['anonymous']}" /> </bean> <bean class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" id="expressionHandler"/> <bean class="org.springframework.security.web.access.expression.WebExpressionVoter" id="expressionVoter"> <property name="expressionHandler" ref="expressionHandler"/> </bean>
3.12 其他的工具bean
<util:properties id="securityKeys"> <prop key="remember-me">182301IEKO1L73C181891TLTKABCNKA1956A7G9UPQXN</prop> <prop key="anonymous">BF93JFJ091N00Q7HF</prop> </util:properties> <alias name="userDao" alias="hibernateUserDetailsService"/> <bean class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" id="md5PasswordEncoder" /> <!-- 如果使用 Sha加密的话,可以用一用 --> <!-- <bean class="org.springframework.security.authentication.dao.ReflectionSaltSource" id="saltSource"> <property name="userPropertyToUse" value="id"/> </bean> -->
(四) How to
4.1 在MVC框架里,我该如何得到现在已经登录的用户?
User currentUser = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
4.2 登录页面我要做后端验证,我该怎么办?
usernamePasswordProcessingFilter实际完成这个功能,自己写这个类。
public class ValidatedUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private Logger logger = LoggerFactory.getLogger(getClass()); @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { Authentication result = null; Locale locale = request.getLocale(); try { result = super.attemptAuthentication(request, response); logger.debug("登录成功"); } catch (AuthenticationException failed) { logger.debug("登录失败"); String msg = super.messages.getMessage("validator.login.fail", locale); request.setAttribute("fail", msg); throw failed; } return result; } } // 注意这个类是实现了MessageSourceAware接口的。你加载了国际化文件的MessageSource用就好了。但是一定要配置在 WebApplicationContext里,不要配置在springMVC特有的配置文件里。