以上省略了创建对象及DAO等基本操作
五 、权限控制
以上内容我们只解决了用户登录问题,但是实际开发中仅仅完成用户登录是不够的,我们还需要用户授权及授权验证。由于我们已经将用户信息存储到数据库里了,那么姑且我们也将权限信息存储在数据库吧。
1. 准备数据库表及测试数据
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
- role:角色信息表,permission权限信息表,user_r_role用户所属角色表,role_r_permission角色拥有权限表,user_r_permission用户拥有权限表。
- 由于用户有所属角色且角色是有权限的,用户同时又单独拥有权限,所以用户最终拥有的权限取并集。
- 用户admin最终拥有角色adminRole以及权限:permission1、permission2、permission3、permission4
- 用户guest最终拥有角色guestRole以及权限:permission3、permission4
2. Dao、Service
dao增加方法:根据用户名查角色以及根据用户名查权限
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
service增加方法:根据用户名查角色以及根据用户名查权限
| 1 2 3 4 5 6 7 |
|
3. WebSecurityConfig
(1)调整public UserDetailsService userDetailsService()方法,在构建用户信息的时候把用户所属角色和用户所拥有的权限也填充上(最后return的时候)。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
这里面有个坑,就是红色代码部分。具体可查看org.springframework.security.core.userdetails.User.UserBuilder。roles()方法和authorities()方法实际上都是在针对UserBuilder的authorities属性进行set操作,执行roles("roleName")和执行authorities("ROLE_roleName")是等价的。所以上例代码中roles(roles.toArray(roleArr))起不到任何作用,直接被后面的authorities(permissions.toArray(permissionArr))覆盖掉了。
所以正确的写法可参考:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
(2)增加新的bean,为我们需要的保护的接口设定需要权限验证:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
- /guest/**的接口会被允许所有人访问,包括未登录的人。
- /admin/**的接口只能被拥有admin角色的用户访问。
- /authenticated/**的接口可以被所有已经登录的用户访问。
- /permission1/**的接口可以被拥有permission1权限的用户访问。/permission2/**、/permission3/**、/permission4/**同理
4. TestController
最后我们调整下TestContrller,增加几个接口以便测试:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
5. 测试
- 访问/guest/getData无需登录即可访问成功。
- 访问/authenticated/getData,会弹出用户登录页面。登录任何一个用户都可访问成功。
- 访问/admin/getData,会弹出用户登录页面。登录admin用户访问成功,登录guest用户会发生错误,403未授权。
- 其他的就不再赘述了。
六、自定义登录页面
是不是觉得SpringScurity的登录页面丑爆了?是不是想老子还能做一个更丑的登录页面你信不信?接下来我们来弄一个更丑的登录页面。
1. 增加pom依赖
| 1 2 3 4 |
|
2. 编写自己的登录页面
thymeleaf默认的页面放置位置为:classpath:templates/ 目录下,所以在编写代码的时候我们可以将页面放在resources/templates目录下,名称为:login.html:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
3. 将SpringSecurity指向自定义的登录页面
(1)调整WebSecurityConfig注入的WebSecurityConfigurerAdapter,在and().formLogin()后面增加loginPage("/login")以指定登录页面的uri地址,同时关闭csrf安全保护。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
(2)TestController增加login方法(注意我们之前在TestController类上注解了@RestController,这里要记得改成@Controller,否则访问/login的时候会直接返回字符串而不是返回html页面。另外除了下面新增的/login方法其他方法要增加注解@ResponseBody)
| 1 2 3 4 |
|
4. 测试及其他
测试过程就略吧。还有一些要嘱咐的东西给小白们:
- 我们通过loginPage("/login")来告知SpringSecurity自定义登录页面的uri路径,同时这个设定也告知了用户点击登录按钮的时候form表单post的uri路径。即:如果SpringSecurity判定需要用户登录,会将302到/login (get请求),用户输入用户名和密码点击登录按钮后,也需要我们自定义页面post到/login才能让SpringSecurity完成用户认证过程。
- 关于html中输入用户名的input的name属性值本例为username、输入密码的input的name属性值本例为password,这是因为SpringSecurity在接收用户登录请求时候默认的参数名就是username和password、如果想更改这两个参数名,可以这样设定:and().formLogin().loginPage("/login").usernameParameter("username").passwordParameter("password")
- 测试过程中我们可以试着输错用户名和密码点击登录,会发现页面又重新跳转到 http://127.0.0.1:8080/login?error ,只不过后面增加了参数error且没有参数值。所以需要我们再login.html中处理相应的逻辑。当然你也可以指定用户认证失败时候的跳转地址,可以这样设定:and().formLogin().loginPage("/login").failureForwardUrl("/login/error")
- 测试过程中,如果我们直接访问http://127.0.0.1:8080/login,输入正确的用户名和密码后跳转到http://127.0.0.1:8080即网站根目录。如果你想指定用户登录成功后的默认跳转地址,可以这样设定:and().formLogin().loginPage("/login").successForwardUrl("/login/success")
七、登出
登出呢?有登录了,怎么能没有登出呢?其实SpringSecurity已经早早的为我们默认了一个登出功能,你访问:http://127.0.0.1:8080/logout 试试看?
如果想做我们自己的个性化登出,可以继续调整WebSecurityConfig注入的WebSecurityConfigurerAdapter
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
MyLogoutHandle实现了LogoutHandler接口:
| 1 2 3 4 5 6 7 8 9 |
|
•MyLogoutSuccessHandle实现了LogoutSuccessHandler接口:
| 1 2 3 4 5 6 7 8 9 |
|
- logoutUrl():告诉SpringSecurity用户登出的接口uri地址是什么
- logoutSuccessUrl():告诉SpringSecurity完成用户登出后要跳转到哪个地址。如果设定了LogoutSuccessHandler则logoutSuccessUrl设定无效
- invalidateHttpSession:执行登出的同时是否清空session
- deleteCookies:执行登出的同时删除那些cookie
- addLogoutHandler:执行登出的同时执行那些代码
八、SpringSecurity在Restfull中的变通使用
当前环境前后盾分离已经是大趋势了吧,除非那些很小很小的项目。所以SpringBoot项目更多的时候为前端提供接口,而并不提供前端页面路由的功能。所以,当SpringSecurity在Restfull开发中还需要变通一下:
1.首先我们通过and().formLogin().loginPage("/login")设定的跳转到登录页面的GET请求不再指向html,而是直接返回json数据告知前端需要用户登录。
2.用户执行登录的时候,前端执行post请求到/login进行用户身份校验。
3.然后我们通过and().formLogin().failureForwardUrl("/login/error")和and().formLogin().successForwardUrl("/login/error")设定的登录成功和失败跳转来地址来返回json数据给前端告知其用户认证结果。
4.最后我们通过and().logout().logoutSuccessHandler(new MyLogoutSuccessHandle())来返回json数据给前端告知用户已经完成登出。
九、SpringSecurity+SpringSession+Redis
接下来还有一个问题要处理。在上面的案例中,session都是存储在servlet容器中的,如果我们需要多点部署负载均衡的话,就会出现问题。比如:我们部署了两个服务并做了负载均衡,用户登录时调用其中一台服务进行身份认证通过并将用户登录信息存储在了这台服务器的session里,接下来用户访问其他接口,由于负载均衡的存在用户请求被分配到了另一个服务上,该服务检测用户session不存在啊,于是就拒绝访问。
在SpringBoot环境下解决这个问题也很简答,很容易就想到SpringSession。所以我们尝试用SpringSession+Redis解决此问题
1. 增加pom依赖
| 1 2 3 4 5 6 7 8 |
|
2. 修改application.yml
| 1 2 3 4 5 6 7 8 |
|
3. 修改主启动类,增加@EnableRedisHttpSession注解,开启SpringSession
十、通过注解的方式实现权限控制
首先要在主启动类上增加@EnableGlobalMethodSecurity注解,具体参数如下:
1.@EnableGlobalMethodSecurity(securedEnabled=true)
支持@Secured注解,例如
| 1 |
|
2.@EnableGlobalMethodSecurity(jsr250Enabled=true)
支持@RolesAllowed、@DenyAll、@PermitAll 注解,例如:
| 1 |
|
3.@EnableGlobalMethodSecurity(prePostEnabled=true)
支持@PreAuthorize、@PostAuthorize、@PreFilter、@PostFilter注解,它们使用SpEL能够在方法调用上实现更有意思的安全性约束
- @PreAuthorize :在方法调用之前,基于表达式的计算结果来限制对方法的访问,只有表达式计算结果为true才允许执行方法
- @PostAuthorize 在方法调用之后,允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常
- @PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果
- @PreFilter 允许方法调用,但必须在进入方法之前过滤输入值
由于这里涉及到SpEL表达式,所以本文就不详细说了。
十一、在Controller中获取当前登录用户
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
十二、总结
SpringSecurity的使用基本就上面这些。就业务逻辑来说,SpringSecurity中所谓的role概念严格意义并不能称之为“角色”。理由是:如果我们的权限控制比较简单,整个系统中的角色以及角色所拥有的权限是固定的,那么我们可以将SpringSecurity的role概念拿来即用。但是如果我们的权限控制是可配置,用户和角色是多对多关系、角色和权限也是多对多关系,那么我们只能讲SpringSecurity的role当做“权限”来使用。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。
**以上参考方法为用户-角色为1对1,角色-权限为多对多,判断是否能够访问时取得用户角色,同时在资源上硬编码访问角色
1、如果要将资源与角色都存储在数据库中,则再加MyInvocationSecurityMetadataSourceService
@Service
public class MyInvocationSecurityMetadataSourceService implements
FilterInvocationSecurityMetadataSource {
@Autowired
private PermissionMapper permissionMapper;
private HashMap<String, Collection<ConfigAttribute>> map =null;
/**
* 加载权限表中所有权限
*/
public void loadResourceDefine(){
map = new HashMap<String, Collection<ConfigAttribute>>();
Collection<ConfigAttribute> array;
ConfigAttribute cfg;
List<Permission> permissions = permissionMapper.selectAll();
for(Permission permission : permissions) {
array = new ArrayList<ConfigAttribute>();
cfg = new SecurityConfig(permission.getName());
//此处只添加了用户的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
array.add(cfg);
//用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value,
map.put(permission.getUrl(), array);
}
}
//此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
if(map ==null) loadResourceDefine();
//object 中包含用户请求的request 信息
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
AntPathRequestMatcher matcher;
String resUrl;
for(Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) {
resUrl = iter.next();
matcher = new AntPathRequestMatcher(resUrl);
if(matcher.matches(request)) {
return map.get(resUrl);
}
}
return null;
}
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
public boolean supports(Class<?> clazz) {
return true;
}
}
2、添加决策器MyAccessDecisionManager
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
// decide 方法是判定是否拥有权限的决策方法,
//authentication 是释CustomUserService中循环添加到 GrantedAuthority 对象中的权限信息集合.
//object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
//configAttributes 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if(null== configAttributes || configAttributes.size() <=0) {
return;
}
ConfigAttribute c;
String needRole;
for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
c = iter.next();
needRole = c.getAttribute();
for(GrantedAuthority ga : authentication.getAuthorities()) {//authentication 为在注释1 中循环添加到 GrantedAuthority 对象中的权限信息集合
if(needRole.trim().equals(ga.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("no right");
}
public boolean supports(ConfigAttribute attribute) {
return true;
}
public boolean supports(Class<?> clazz) {
return true;
}
}
3、再加上拦截器MyFilterSecurityInterceptor
@Service
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
public void destroy() {
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}
4、要注意在securityconfig中注册此拦截器
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
485

被折叠的 条评论
为什么被折叠?



