前面章节的数据并不是完全来自于数据库,比如spring security用户初始只是在xml配置,后来改为数据库了,再有权限一直都是根据工具类虚拟出来的.下面的例子我们继续改进.本文是在chapter04.00-calendar基础上修改而来.
一.基于组的访问控制
JdbcUserDetailsManager支持增加一个在用户与GrantedAuthority的声明之间的间接层,通过分组GrantedAuthority成为被叫做组的逻辑集。
然后用户被分配一个或多个组,其成员赋予一组的GrantedAuthority声明
1.配置好数据源.
表定义与初始化数据
a.security-schema.sql
create table users( username varchar(256) not null primary key, password varchar(256) not null, enabled boolean not null ); create table authorities ( username varchar(256) not null, authority varchar(256) not null, constraint fk_authorities_users foreign key(username) references users(username) ); create unique index ix_auth_username on authorities (username,authority);b.security-users.sql
insert into users (username,password,enabled) values ('[email protected]','user1',1); insert into users (username,password,enabled) values ('[email protected]','admin1',1); insert into users (username,password,enabled) values ('[email protected]','admin1',1); insert into users (username,password,enabled) values ('[email protected]','disabled1',0);c.security-groups-schema.sql
create table groups ( id bigint generated by default as identity(start with 0) primary key, group_name varchar(256) not null ); create table group_authorities ( group_id bigint not null, authority varchar(50) not null, constraint fk_group_authorities_group foreign key(group_id) references groups(id) ); create table group_members ( id bigint generated by default as identity(start with 0) primary key, username varchar(50) not null, group_id bigint not null, constraint fk_group_members_group foreign key(group_id) references groups(id) );d.security-groups-mappings.sql
----- -- Create the Groups insert into groups(group_name) values ('Users'); insert into groups(group_name) values ('Administrators'); ----- -- Map the Groups to Roles insert into group_authorities(group_id, authority) select id,'ROLE_USER' from groups where group_name='Users'; -- Administrators are both a ROLE_USER and ROLE_ADMIN insert into group_authorities(group_id, authority) select id,'ROLE_USER' from groups where group_name='Administrators'; insert into group_authorities(group_id, authority) select id,'ROLE_ADMIN' from groups where group_name='Administrators'; ----- -- Map the users to Groups insert into group_members(group_id, username) select id,'[email protected]' from groups where group_name='Users'; insert into group_members(group_id, username) select id,'[email protected]' from groups where group_name='Administrators'; insert into group_members(group_id, username) select id,'[email protected]' from groups where group_name='Users'; insert into group_members(group_id, username) select id,'[email protected]' from groups where group_name='Users';service.xml
<jdbc:embedded-database id="dataSource" type="H2"> <jdbc:script location="classpath:/database/h2/calendar-schema.sql"/> <jdbc:script location="classpath:/database/h2/calendar-data.sql"/> <jdbc:script location="classpath:/database/h2/security-schema.sql"/> <jdbc:script location="classpath:/database/h2/security-users.sql"/> <jdbc:script location="classpath:/database/h2/security-groups-schema.sql"/> <jdbc:script location="classpath:/database/h2/security-groups-mappings.sql"/> </jdbc:embedded-database>2.配置JdbcUserDetailsManager使用组
<authentication-manager> <authentication-provider> <jdbc-user-service id="userDetailsService" data-source-ref="dataSource" group-authorities-by-username-query= "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id"/> </authentication-provider> </authentication-manager>
3.注册用户时调用
userDetailsManager.createUser(userDetails);
设置SecurityContext调用
UserDetails userDetails = userDetailsService.loadUserByUsername(user.getEmail()); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, user.getPassword(),userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication);4.启动jetty测试
二.支持自定义方案.很多数据库并不符合spring security的数据表定义,但用以过JdbcDaoImpl来映射.
JdbcUserDetailsManager有三条明确的定义参数和返回列的sql查询.
命名空间查询的属性名称 描述期望SQL列
users-by-username-query:返回一个或多个匹配用户名的用户;但只有第一用户被使用.Username (string),Password (string),Enabled (Boolean)
authorities-by-username-query:返回一个或多个直接授予用户的权限;通常用于GBAC被禁用的情况下.Username (string),Granted Authority (string)
group-authorities-by-username-query:返回通过组成员关系的用户的权限和组详细.当GBAC启用时使用.Group Primary Key (any),Group Name (any),Granted Authority (string)
返回列不一定要使用JdbcUserDetailsManager的默认实现,但这些列无论如何都要返回的.
1.再回到之前的例子,我们将所有以security-开头的sql都移掉,加入calendar-authorities.sql,如下:
<jdbc:embedded-database id="dataSource" type="H2"> <jdbc:script location="classpath:/database/h2/calendar-schema.sql"/> <jdbc:script location="classpath:/database/h2/calendar-data.sql"/> <jdbc:script location="classpath:/database/h2/calendar-authorities.sql"/> </jdbc:embedded-database>2.DefaultCalendarService.java
private final JdbcOperations jdbcOperations; @Autowired public DefaultCalendarService(EventDao eventDao, CalendarUserDao userDao, JdbcOperations jdbcOperations) { ... this.jdbcOperations = jdbcOperations; } public int createUser(CalendarUser user) { int userId = userDao.createUser(user); jdbcOperations.update("insert into calendar_user_authorities(calendar_user,authority) values (?,?)", userId, "ROLE_USER"); return userId; }3.配置JdbcUserDetailsManager使用自定义sql查询.
<authentication-provider> <jdbc-user-service id="userDetailsService" data-source-ref="dataSource" users-by-username-query="select email,password,true from calendar_users where email = ?" authorities-by-username-query="select cua.id, cua.authority from calendar_users cu, calendar_user_authorities cua where cu.email = ? and cu.id = cua.calendar_user"/> </authentication-provider>4.启动jetty测试
三.配置安全密码.我们知道,经过加密的密码会更安全.spring security为我们实现了不少编码器,都放在o.s.s.authentication.encoding包下面.
1.声明一个编码器bean
<bean:bean id="passwordEncoder" xmlns="http://www.springframework.org/schema/beans" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder"> <constructor-arg value="256"/> </bean:bean>2.让spring security知道passwordEncoder.在 <authentication-provider>的子元素加入<password-encoder ref="passwordEncoder"/>
DefaultCalendarService,注入passwordEncoder(发现这本书的作者很喜欢用构造器的方法注入bean),然后在createUser方法使用user.setPassword(passwordEncoder.encodePassword(user.getPassword(),null));然后发现新注册的
用户就可以登录了,当然,初始化的那些用户update一下经过编码的密码一样也可以登录.
四.为密码再加点盐.前面的例子,如果用户的密码相同,那么最终保存在数据库的密码数据也是一样的.如果能做到保存不一样,那就更安全了.
将明文密码与适当的盐共同进行加密生成的密文就可以不同.一般的盐无非就两种:
a.使用与用户相关的数据按算法来生成,如用户创建的时间;b.随机生成的,并且与用户的密码一起按照某种形式进行存储
因为盐加入到了明文密码,所以是不能使用单向加密的.因为对于一个指定的用户,
应用需要得到合适的盐值来计算密码的哈希与用户所存储的哈希进行比较来完成认证.
spring security使用盐.spring security3.1的spring-security-core和spring-security-crypto都提供了o.s.s.crypto.password.PasswordEncoder
应优先使用这个接口的方法,因为使用了随机盐.这接口用三实现:
o.s.s.crypto.bcrypt.BCryptPasswordEncoder:防止穷举搜索攻击。
o.s.s.crypto.password.NoOpPasswordEncoder:以明文形式返回密码
o.s.s.crypto.password.StandardPasswordEncoder:使用SHA-256多次迭代和随机盐值
1.更新passwordEncoder如下:
<bean:bean id="passwordEncoder" class="org.springframework.security.crypto.password.StandardPasswordEncoder"/>
2.password-encoder引用不变
3.DefaultCalendarService里面的passwordEncoder注入的是o.s.s.crypto.password.PasswordEncoder类型而不是之前的
o.s.s.authentication.encoding.PasswordEncoder;createUser方法调用使用user.setPassword(passwordEncoder.encode(user.getPassword()));
4.重启jetty测试
再看看这个例子是如何保存密码与认证的:
a.存储密码:先创建一个随机盐,然后将随机盐和原始密码进行哈希,最后将盐与哈希组合存储
salt = randomsalt()
hash = hash(salt+originalPassword)
storedPassword = salt + hash
b.认证:从存储密码得到盐与哈希(这不能是单向加密).然后将得到的盐与输入的密码进行哈希,这两哈希如果相等就通过认证.
storedPassword = datasource.lookupPassword(username)
salt, expectedHash = extractSaltAndHash(storedPassword)
actualHash = hash(salt+inputedPassword)
authenticated = (expectedHash == actualHash)