网上看了很多springsecurity的资料,一堆配置,看的还是蒙的很,打算从代码入手。
慢慢深入摸清套路;
首先,新建一个spring-boot项目。写一个api接口作为后续测试;
@RestController
@RequestMapping(value = "/api")
public class ApiController {
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
return "product id :" + id;
}
}
既然要使用spring security,pom引入依赖;
org.springframework.boot
spring-boot-starter-security
此时,直接启动项目,访问接口会发现,需要你输入登录账号密码,账号默认是user,密码在后台启动界面有显示。
输入账号密码后会正常显示接口信息。
也就是说引入spring-security依赖后,默认所有接口会受到保护,请求会跳到默认的login界面;
既然有默认的账号密码,那么这个账号密码应该也是可配置的,实际项目中,用户的账号密码都是数据库中。那么如何按照我们自己的方式来做账号密码登录呢;
先看看官方的例子:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
// 定义加密方式,不然启动不了
.passwordEncoder(new BCryptPasswordEncoder())
// 设置用户名
.withUser("admin")
// 设置密码(密文密码)
.password(new BCryptPasswordEncoder().encode("123456"))
// 设置角色,不设置启动不了
.roles("");
}
}
他的认证管理器是用内存方式实现的,内存中固定了账号admin,和密码123456;
那么用查数据库中用户信息的方式来认证如何实现呢
Security框架提供了两个接口UserDetails和UserDetailsService。
UserDetails就是获取用户认证相关的信息,需要有个实体类实现相关的方法,例:
public class MyUserBean implements UserDetails {
private Long id;
private String name;
private String address;
private String username;
private String password;
private String roles;
public MyUserBean(Long id, String name, String address, String username, String password, String roles) {
this.id = id;
this.name = name;
this.address = address;
this.username = username;
this.password = password;
this.roles = roles;
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
String[] authorities = roles.split(",");
List simpleGrantedAuthorities = new ArrayList<>();
for (String role : authorities) {
simpleGrantedAuthorities.add(new SimpleGrantedAuthority(role));
}
return simpleGrantedAuthorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetailsService接口仅定义了一个方法loadUserByUsername(String username) ,
这个方法由接口的实现类来具体实现,它的作用就是通过用户名username从数据库中查询,并将结果赋值给一个UserDetails的实现类实例
为了方便测试就直接new了一个用户对象,没有从数据库获取,实际应用中可以根据username或者其他唯一字段从库中查找相关用户信息。
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//MyUserBean userBean = mapper.selectByUsername(username);
MyUserBean userBean = new MyUserBean(9999L,"srf","beijing","666ddd",new BCryptPasswordEncoder().encode("123456"),"ROLE_USER,ROLE_ADMIN");
if (userBean == null) {
throw new UsernameNotFoundException("数据库中无此用户!");
}
return userBean;
}
}
然后将认证管理器改为userDetailsService即可
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//加入数据库验证类,下面的语句实际上在验证链中加入了一个DaoAuthenticationProvider
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
此时,用户账号密码是要和loadUserByUsername返回一致即可完成验证;
以上就是认证的过程,实际应用中设计好自己的用户表,按照以上方法可以简单实现一个认证登录了。
接下来需要看的是授权的部分,
例如一个后台系统,包含了财务模块,用户模块等等。。。。
对于不同的登录者,他们能看到的模块,或者说界面,能访问的url是不同的。那么如何做到url和登录者用户的权限的匹配呢。
同样需要重写WebSecurityConfigurerAdapter的configure(HttpSecurity http)
先看下源码中默认的方式:
这段的意思就是说所有的请求都需要进行认证到登录界面,所以我们引入spring-security后默认所有的接口都被保护了,需要登录才能访问。
protected void configure(HttpSecurity http) throws Exception {
this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated().and()).formLogin().and()).httpBasic();
}
现在,我们重写该方法,给之前的测试接口加一个权限验证,及/api/product/**这个路径必须是登陆者有ROLE_USER权限才能访问(默认会有ROLE_前缀)。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/product/**").hasRole("USER")
.anyRequest().authenticated()
.and().formLogin()
.and().httpBasic();
}
这些基本的表达式我来给大家介绍下
表达式 | 备注 |
---|---|
hasRole | 用户具备某个角色即可访问资源 |
hasAnyRole | 用户具备多个角色中的任意一个即可访问资源 |
hasAuthority | 类似于 hasRole |
hasAnyAuthority | 类似于 hasAnyRole |
permitAll | 统统允许访问 |
denyAll | 统统拒绝访问 |
isAnonymous | 判断是否匿名用户 |
isAuthenticated | 判断是否认证成功 |
isRememberMe | 判断是否通过记住我登录的 |
isFullyAuthenticated | 判断是否用户名/密码登录的 |
principle | 当前用户 |
authentication | 从 SecurityContext 中提取出来的用户对象 |
这种是表达式控制 URL 路径权限的方法。
而我们实际项目中,动态权限应该是用的最多的。以下详情参考大佬文章:https://zhuanlan.zhihu.com/p/47873694
应该会有权限表,有对应url所需的权限关系,并且可配置的。
主要通过重写拦截器和决策器来实现动态的权限控制;
用户访问某资源/xxx时,FilterInvocationSecurityMetadataSource这个类的实现类(本文是MySecurityMetadataSource)会调用getAttributes方法来进行资源匹配。它会读取数据库resource表中的所有记录,对/xxx进行匹配。若匹配成功,则将/xxx对应所需的角色组成一个 Collection
流程来到鉴权的决策类AccessDecisionManager的实现类(MyAccessDecisionManager)中,它的decide方法可以决定当前用户是否能够访问资源。decide方法的参数中可以获得当前用户的验证信息、第3步中获得的资源所需角色信息,对这些角色信息进行匹配即可决定鉴权是否通过。当然,你也可以加入自己独特的判断方法,例如只要用户具有ROLE_ADMIN角色就一律放行;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor() {
@Override
public O postProcess(O object) {
object.setSecurityMetadataSource(mySecurityMetadataSource);
object.setAccessDecisionManager(myAccessDecisionManager);
return object;
}
});
}
自定义的元数据源类,用来提供鉴权过程中,访问资源所需的角色:
@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
ResourceMapper resourceMapper;
//本方法返回访问资源所需的角色集合
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection getAttributes(Object object) throws IllegalArgumentException {
//从object中得到需要访问的资源,即网址
String requestUrl = ((FilterInvocation) object).getRequestUrl();
//从数据库中得到所有资源,以及对应的角色
List resourceBeans = resourceMapper.selectAllResource();
for (MyResourceBean resource : resourceBeans) {
//首先进行地址匹配
if (antPathMatcher.match(resource.getUrl(), requestUrl)
&& resource.getRolesArray().length > 0) {
return SecurityConfig.createList(resource.getRolesArray());
}
}
//匹配不成功返回一个特殊的ROLE_NONE
return SecurityConfig.createList("ROLE_NONE");
}
@Override
public Collection getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
本类是鉴权的决策类:
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
//从authentication中获取当前用户具有的角色
Collection extends GrantedAuthority> userAuthorities = authentication.getAuthorities();
//从configAttributes中获取访问资源所需要的角色,它来自MySecurityMetadataSource的getAttributes
Iterator iterator = configAttributes.iterator();
while (iterator.hasNext()) {
ConfigAttribute attribute = iterator.next();
String role = attribute.getAttribute();
if ("ROLE_NONE".equals(role)) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new BadCredentialsException("用户未登录");
} else
return;
}
//逐一进行角色匹配
for (GrantedAuthority authority : userAuthorities) {
if (authority.getAuthority().equals("ROLE_ADMIN")) {
return; //用户具有ROLE_ADMIN权限,则可以访问所有资源
}
if (authority.getAuthority().equals(role)) {
return; //匹配成功就直接返回
}
}
}
//不能完成匹配
throw new AccessDeniedException("你没有访问" + object + "的权限!");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class> clazz) {
return true;
}
}
相关权限表sql
-- ---------------------------- -- Table structure for `user` -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(32) DEFAULT NULL COMMENT '姓名', `address` varchar(64) DEFAULT NULL COMMENT '联系地址', `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号', `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码', `roles` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '角色', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of `user` -- ---------------------------- BEGIN; INSERT INTO `user` VALUES ('1', 'Adam', 'beijing', 'adam','$2a$10$9SIFu8l8asZUKxtwqrJM5ujhWarz/PMnTX44wXNsBHfpJMakWw3M6', 'ROLE_USER'); INSERT INTO `user` VALUES ('2', 'SuperMan', 'shanghang', 'super','$2a$10$9SIFu8l8asZUKxtwqrJM5ujhWarz/PMnTX44wXNsBHfpJMakWw3M6', 'ROLE_USER,ROLE_ADMIN'); INSERT INTO `user` VALUES ('3', 'Manager', 'beijing', 'manager','$2a$10$9SIFu8l8asZUKxtwqrJM5ujhWarz/PMnTX44wXNsBHfpJMakWw3M6', 'ROLE_USER,ROLE_MANAGER'); INSERT INTO `user` VALUES ('4', 'User1', 'shanghang', 'user1','$2a$10$9SIFu8l8asZUKxtwqrJM5ujhWarz/PMnTX44wXNsBHfpJMakWw3M6', 'ROLE_USER,ROLE_DEPART1'); INSERT INTO `user` VALUES ('5', 'User2', 'shanghang', 'user2','$2a$10$9SIFu8l8asZUKxtwqrJM5ujhWarz/PMnTX44wXNsBHfpJMakWw3M6', 'ROLE_USER,ROLE_DEPART2'); COMMIT; -- ---------------------------- -- Table structure for `resource` -- ---------------------------- DROP TABLE IF EXISTS `resource`; CREATE TABLE `resource` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `url` varchar(255) DEFAULT NULL COMMENT '资源', `roles` varchar(255) DEFAULT NULL COMMENT '所需角色', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of `resource` -- ---------------------------- BEGIN; INSERT INTO `resource` VALUES ('1', '/depart1/**', 'ROLE_ADMIN,ROLE_MANAGER,ROLE_DEPART1'); INSERT INTO `resource` VALUES ('2', '/depart2/**', 'ROLE_ADMIN,ROLE_MANAGER,ROLE_DEPART2'); INSERT INTO `resource` VALUES ('3', '/user/**', 'ROLE_ADMIN,ROLE_USER'); INSERT INTO `resource` VALUES ('4', '/admin/**', 'ROLE_ADMIN'); COMMIT;
按照以上便能够简单实现不同角色的授权过程