所谓的最简,实际上就是尽量利用现有资源,实现一个可管理的权限后台。
我们将使用Spring Security提供的filter实现URL级的权限控制,使用Spring Security提供的UserDetailsManager实现用户管理,其中会包含用户密码加密和用户信息缓存。麻雀虽小,五脏俱全,如果想为自己的系统添加最简的权限后台,这一章将是不二之选。
我们将在这个简易的控制台中实现如下功能:浏览用户,新增用户,修改用户,删除用户,修改密码,用户授权。
选择maven2作为主要的构建工具,以便更加方便的管理第三方依赖,这章使用的依赖如下所示:
com.family168.springsecuritybook:ch101:war:0.1 +- org.springframework.security:spring-security-taglibs:jar:2.0.4:compile | +- org.springframework.security:spring-security-core:jar:2.0.4:compile | | +- org.springframework:spring-core:jar:2.0.8:compile | | +- org.springframework:spring-context:jar:2.0.8:compile | | | \- aopalliance:aopalliance:jar:1.0:compile | | +- org.springframework:spring-aop:jar:2.0.8:compile | | +- org.springframework:spring-support:jar:2.0.8:runtime | | +- commons-logging:commons-logging:jar:1.1.1:compile | | +- commons-codec:commons-codec:jar:1.3:compile | | \- commons-collections:commons-collections:jar:3.2:compile | +- org.springframework.security:spring-security-acl:jar:2.0.4:compile | | \- org.springframework:spring-jdbc:jar:2.0.8:compile | | \- org.springframework:spring-dao:jar:2.0.8:compile | \- org.springframework:spring-web:jar:2.0.8:compile | \- org.springframework:spring-beans:jar:2.0.8:compile +- hsqldb:hsqldb:jar:1.8.0.7:compile +- javax.servlet:servlet-api:jar:2.4:provided +- taglibs:standard:jar:1.1.2:compile +- javax.servlet:jstl:jar:1.1.2:compile \- net.sf.ehcache:ehcache:jar:1.6.0:compile
项目的目录结构如下:
+ ch101/ + src/ + main/ + java/ + com/ + family168/ + springsecuritybook/ + ch101 * UserBean.java * UserManager.java * UserServlet.java + resources/ + hsqldb/ * test.properties * test.scripts * applicationContext-security.xml * applicatoinContext-service.xml + webapp/ + includes/ * error.jsp * header.jsp * message.jsp * meta.jsp * taglibs.jsp + scripts/ * jquery.min.js * jquery.validate.pack.js * messages_cn.js + WEB-INF/ * web.xml * index.jsp * login.jsp * user-changePassword.jsp * user-create.jsp * user-edit.jsp * user-list.jsp * user-view.jsp + test/ + resources/ * pom.xml
用户需要登录系统才能进入系统进行操作。有关自定义登录页面的介绍,请参考之前的章节。???
我们为了演示的需要预设了两个用户admin/admin和user/user,打开演示用的数据库文件test.scripts可以看到用户信息以及加密过的用户密码。
INSERT INTO USERS VALUES('admin','ceb4f32325eda6142bd65215f4c0f371',TRUE) INSERT INTO USERS VALUES('user','47a733d60998c719cf3526ae7d106d13',TRUE) INSERT INTO AUTHORITIES VALUES('admin','ROLE_ADMIN') INSERT INTO AUTHORITIES VALUES('admin','ROLE_USER') INSERT INTO AUTHORITIES VALUES('user','ROLE_USER')
为了提升安全等级,我们对密码使用了md5和saltValue进行加密,对应的配置文件在applicationContext-securit.xml中。
<authentication-provider user-service-ref="userDetailsManager"> <password-encoder ref="passwordEncoder"> <salt-source user-property="username"/> </password-encoder> </authentication-provider>
用户登录成功之后即进入用户信息列表。
显示所有用户信息的请求地址为/user.do?action=list,这个请求将交由UserServlet.java处理,在list()方法中调用UserManager.java的getAll()方法获得数据库中所有的用户信息。
/** * get all of user. */ public List<UserBean> getAll() { String sql = "select username,password,enabled,authority" + " from users u inner join authorities a on u.username=a.username"; List<Map> list = jdbcTemplate.queryForList(sql); List<UserBean> userList = new ArrayList<UserBean>(); UserBean ub = null; for (Map map : list) { if (ub == null) { ub = new UserBean((String) map.get("username"), (String) map.get("password"), (Boolean) map.get("enabled")); ub.addAuthority((String) map.get("authority")); } else if (ub.getUsername().equals(map.get("username"))) { ub.addAuthority((String) map.get("authority")); } else { userList.add(ub); // ub = new UserBean((String) map.get("username"), (String) map.get("password"), (Boolean) map.get("enabled")); ub.addAuthority((String) map.get("authority")); } } if (!list.isEmpty()) { userList.add(ub); } return userList; }
getAll()方法中将数据库中所有的用户信息取出来,并将用户信息和对应的权限组装在一起,并将获得的数据提交给user-list.jsp进行显示。出于安全性的考虑,即使密码已经经过了加密,我们还是选择在页面上不显示用户的密码。
点击页面左上角的Create User可以进入添加用户的界面。
如果操作成功,会向数据库中添加一条用户记录,以及对应的权限信息,并在用户列表页面中显示成功提示。
如果操作失败,会跳转到添加页面,并显示错误信息。
添加用户操作过程中,UserServlet.java中的save()方法负责请求跳转与数据校验,UserManager.java中的save()方法负责将提交的保存入数据库。
/** * create a new user and insert he to database. */ public void save(String username, String password, boolean enabled, String[] authorities) { GrantedAuthority[] gas = new GrantedAuthority[authorities.length]; for (int i = 0; i < authorities.length; i++) { gas[i] = new GrantedAuthorityImpl(authorities[i].trim()); } String encodedPassword = passwordEncoder.encodePassword(password, username); UserDetails ud = new User(username, encodedPassword, enabled, true, true, true, gas); userDetailsManager.createUser(ud); }
这里我们需要注意两个部分。
第一部分,需要将用户拥有的权限转换为GrantedAuthority数组,并赋予添加的用户。
第二部分,我们在保存用户密码之前,要将提交的明文密码使用passwordEncoder进行加密,encodePassword()方法会使用我们设置的加密算法,并使用username作为saltValue对密码进行加密。
最终,我们将转化后的数据组装为一个UserDetails对象,并保存到数据库中,以此完成整个添加用户的操作。
在用户列表页面选择一条用户信息进行修改。
修改操作与UserManager.java中的update()方法对应。
/** * update a user information, includes username, enabled or authorities. */ public void update(String username, boolean enabled, String[] authorities) { GrantedAuthority[] gas = new GrantedAuthority[authorities.length]; for (int i = 0; i < authorities.length; i++) { gas[i] = new GrantedAuthorityImpl(authorities[i].trim()); } UserDetails oldUserDetails = userDetailsManager.loadUserByUsername(username); UserDetails ud = new User(username, oldUserDetails.getPassword(), enabled, oldUserDetails.isAccountNonExpired(), oldUserDetails.isAccountNonLocked(), oldUserDetails.isCredentialsNonExpired(), gas); userDetailsManager.updateUser(ud); }
因为修改用户信息不包含修改用户密码,所以此处不需要进行密码加密,这里可以放心调用UserDetailsManager中提供的updateUser()方法,它会帮我们维护用户缓存。
可以查看指定用户的详细信息。
删除用户操作与上述操作基本类似,UserDetailsManager会帮我们处理用户缓存的问题,不用担心出现脏数据。
用户列表右上角有一个Change Password的链接,它用来修改当前登录系统用户的密码。
在UserManager.java内部,我们会调用一个名为changePassword()的方法,这个方法需要两个参数oldPassword和newPassword,这时因为修改密码之前需要先对当前用户进行认证,所以可以在代码中看到,我们为oldPassword和newPassword都进行了加密操作。
/** * let current user change password. */ public void changePassword(String oldPassword, String newPassword) { UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext() .getAuthentication() .getPrincipal(); String username = userDetails.getUsername(); String encodedOldPassword = passwordEncoder.encodePassword(oldPassword, username); String encodedNewPassword = passwordEncoder.encodePassword(newPassword, username); userDetailsManager.changePassword(encodedOldPassword, encodedNewPassword); }
这个方法可以看做是使用编程方式获得当前登录用户信息的一个典型范例,在系统的任何部分,我们都可以使用这样的方式直接获得SecurityContext中保存的登录用户信息。
至此,我们基于Spring Security完成了一个完整的权限管理系统后台,实例代码在ch401。