如题security管理权限的方式有很多,这里我采用的是使用内存管理资源权限,token存放到数据库中返回前端cookie的是
remember-me信息.
pom.xml 主要用的到jar. 这里不再添加数据库部分(就是很常规的用户,角色,权限 五张基本表. 要说的是如果业务需求权限和资源必须分开的话就加张资源表即可, 具体看业务. 我这里权限和资源是放到一起的. 资源主要就是控住url的,如果想拥有某个路径下的所有权限使用 (例:/user/**) 即可访问user下所有的方法).
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
security 简单的理解可以分为认证和授权两个部分.
认证 : 即登录页面获取账号密码和数据库对比如果成功就可以访问系统.
授权 : 查询当前成功登录用户下的角色及所有可以访问的资源路径.
spingboot security核心的就是 WebSecurityConfigurerAdapter 只需要基于配置类我们可以实现最简单的认证授权(往往项目需求都不仅限于此)
SecurityConfig 继承 WebSecurityConfigurerAdapter
@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
@Value("${remember-me}")
private String REMEMBER_ME;
@Autowired
DataSource dataSource;
@Bean
//注册UserDetailsService 的bean
MyUserDetailService wjcUserDetailsService(){
return new MyUserDetailService();
}
// security在查询数据库对比密码时候会调用加密BCryptPasswordEncoder加密每次加密的结果都不一样但解出来的明文都一样
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//user Details Service验证
//密码加密,与数据库匹配
auth.userDetailsService(wjcUserDetailsService()).passwordEncoder(BCryptPasswordEncoder());
}
// 异常处理处理权限异常的自定义内容
@Bean
public AccessDeniedHandler getAccessDeniedHandler() {
return new MyAccessDeniedHandler();
}
// security最精髓的配置基于这个配置就可以完成一个写死的登录认证授权管理,当然这里使用的是数据库方式
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 设置不拦截的页面和静态资源
.antMatchers("/login", "/test","/login/login.html","/css/**","/js/**","/img/**").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
// 自定义异常处理
.accessDeniedHandler(getAccessDeniedHandler());
http.formLogin()
// 设置登录跳转的页面(不设置会跳转security默认的登录页面)
.loginPage("/login")
.defaultSuccessUrl("/index", true)
// 自定义登录页面的表单提交地址如果自定义了登录页面表单提交地址必须和此处一致
.loginProcessingUrl("/dologin") //登录请求
.and()
// 登录点击记住我如果是true就生成token,false就不执行 (提交表单选中)
.rememberMe()
// 使用自定义认证
.userDetailsService(wjcUserDetailsService())
// 生成token保存到数据库
.tokenRepository(persistentTokenRepository())
.and()
.csrf().disable();
//session管理,失效后跳转登录页面
http.sessionManagement().invalidSessionUrl("/login");
//防止iframe
http.headers().frameOptions().disable();
//退出
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
.deleteCookies("JESSIONID")
.permitAll(); //注销行为任意访问
//解决中文乱码问题
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8"); filter.setForceEncoding(true);
http.addFilterBefore(filter, CsrfFilter.class);
// 配置自定义拦截器
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
}
//配置Spring Security的Filter链
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/static/**");
super.configure(web);
}
/**
* 注入密码编解码
* @return
*/
@Bean
public BCryptPasswordEncoder BCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
/**
*
* 配置TokenRepository
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 配置数据源
jdbcTokenRepository.setDataSource(dataSource);
// 第一次启动的时候自动建表(可以不用这句话,自己手动建表,源码中有语句的)
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
MyUserDetailService 实现 UserDetailsService接口完成认证操作
@Slf4j
@Component
public class MyUserDetailService implements UserDetailsService {
@Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
PermissionMapper permissionMapper;
@Autowired
UserService userService;
/**
* 所有错误信息对应异常 我只用了默认的BadCredentialsException异常
* UsernameNotFoundException(用户不存在)
* DisabledException(用户已被禁用)
* BadCredentialsException(坏的凭据)
* LockedException(账户锁定)
* AccountExpiredException (账户过期)
* CredentialsExpiredException(证书过期)
* @param userName
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
User user = userService.selectUser(userName);
if (null == user) {
log.error("用户{},不存在!", userName);
throw new BadCredentialsException("帐号不存在,请重新输入!");
} else {
// 查询用户权限集合
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
List<Permission> allpms = permissionMapper.findByUserName(user.getUsername());
for (Permission permission : allpms) {
if (permission != null && permission.getRoleName()!=null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getRoleName());
//1:此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。
grantedAuthorities.add(grantedAuthority);
}
}
user.setGrantedAuthorities(grantedAuthorities);
}
// 自定义业务逻辑校验
/*if ("userli".equals(user.getUsername())) {
log.error("用户{},受限制!", userName);
throw new BadCredentialsException("当前账号限制登录!");
}*/
// 权限校验...
user.setPassWord(bCryptPasswordEncoder.encode(user.getPassword()));
log.info("用户{},登录成功.."+user.getUsername());
return user;
}
}
User 实现 UserDetails接口
@Data
public class User implements UserDetails {
private String id;
private String userName;
private String passWord;
private String name;
private String thone;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private Date createTime;
private String status;
private String roleId;
private String userRoleId;
private String showName;
public void setcreateTime(Date createTime){
this.createTime=createTime;
}
private List<GrantedAuthority> grantedAuthorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return grantedAuthorities;
}
@Override
public String getPassword() {
return this.passWord;
}
@Override
public String getUsername() {
return this.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;
}
}
前端登录表单
<form action="dologin" method="post">
<div class="login-input">
<input type="text" name="username" placeholder="用户名/手机号" autocomplete="off">
</div>
<div class="login-input">
<input type="password" name="password" placeholder="密码" autocomplete="off">
</div>
<div style="padding: 15px 17px 0 17px;">
<input type="checkbox" name="remember-me" checked="checked"/>
<span style="color: white;">记住密码</span>
</div>
<div class="login-btn">
<div class="login-btn-left">
<input type="submit" value="登录"/>
</div>
</div>
</form>
到此认证就做完了,认证成功后就会在数据库persistent_logins表中看到登录用户,如果不勾选记住我是不会在这张表中插入信息的,退出登录会自动删除信息默认保存时间好像是15天,浏览器保存有cookie就可以免登陆了.有写cookie是可以通过特殊手段获取敏感信息,使用这种方式不会从cookie中拿到敏感信息.
认证成功后肯定要授权了,证明用户有哪些权限可以访问哪些资源.
MyFilterSecurityInterceptor 继承 AbstractSecurityInterceptor 实现 Filter security核心思想就是过滤器拦截器
自定义的拦截器我们在SecurityConfig中已经配置过了
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
// 所有没有配置放行的静态非静态资源访问都需要经过这个拦截器
@Override
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);
}
}
@Override
public void destroy() {
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}
MyAccessDecisionManager 实现 AccessDecisionManager
@Slf4j
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
@Value("${superAdmin}")
String superAdmin;
//decide 方法是判定是否拥有权限的决策方法,
//authentication 是释CustomUserService中循环添加到 GrantedAuthority 对象中的权限信息集合.
//object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
//configAttributes 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,
//此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。
//如果不在权限表中则放行。
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
if(null== collection || collection.size() <=0) {
return;
}
// 特殊用户superAdmin直接放行 首页直接放行 我自己设定的superAdmin拥有任何权限
if(superAdmin.equals(authentication.getName()) || "/index".equals(((FilterInvocation) o).getHttpRequest().getRequestURI())){
return;
}
ConfigAttribute c;
String needRole;
for(Iterator<ConfigAttribute> iter = collection.iterator(); iter.hasNext(); ) {
c = iter.next();
needRole = c.getAttribute();
//authentication 为在注释1 中循环添加到 GrantedAuthority 对象中的权限信息集合
for(GrantedAuthority ga : authentication.getAuthorities()) {
if(needRole.trim().equals(ga.getAuthority())) {
return;
}
}
}
log.info("权限不足, {} 暂无法访问!",authentication.getName());
// 该异常可以通过监听捕捉使用我们自定义的异常返回页面
throw new AccessDeniedException("权限不足,暂无法访问!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
MyInvocationSecurityMetadataSourceService 实现 FilterInvocationSecurityMetadataSource
从名字就可以看出管理所有资源的它是最先被加载的如果这里面的getAttributes() 没有返回权限集合的话security是不会帮你管理资源的.因为security也不知道你有这样的资源.
@Component
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
@Value("${superAdmin}")
private String superAdmin;
@Autowired
private PermissionMapper permissionMapper;
public static HashMap<String, Collection<ConfigAttribute>> map = null;
/**
* 加载权限表中所有权限
*/
public void loadResourceDefine(){
this.map = new HashMap<>();
Collection<ConfigAttribute> array;
ConfigAttribute cfg = null;
List<Permission> permissions = permissionMapper.findAll();
for(Permission permission : permissions) {
array = new ArrayList<>();
String roleName = permission.getRoleName();
if(StringUtils.isEmpty(roleName)){
roleName = superAdmin;
}
cfg = new SecurityConfig(roleName);
if(this.map.containsKey(permission.getUrl())){
this.map.get(permission.getUrl()).add(cfg);
} else {
array.add(cfg);
this.map.put(permission.getUrl(), array);
}
//此处只添加了角色的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。
//此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
//用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value,
}
}
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
if(map == null)
loadResourceDefine();
//object 中包含用户请求的request 信息
HttpServletRequest request = ((FilterInvocation) o).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;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
MyAccessDeniedHandler 实现 AccessDeniedHandler
该类负责捕捉权限异常,自定义权限异常后抛出AccessDeniedException就可以捕捉到,这里我没有设计华丽的失败页面,就返回了json串.
@Slf4j
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest req, HttpServletResponse rep, AccessDeniedException e) throws IOException, ServletException {
//返回json形式的错误信息
rep.setCharacterEncoding("UTF-8");
rep.setContentType("application/json");
rep.getWriter().println(e.getMessage());
rep.getWriter().flush();
}
}
至此spingboot 整合security 实现用户认证授权就全部完成了,不是特别完美还有待优化,不过基本没啥问题.
有一点值得一提的是FilterInvocationSecurityMetadataSource中定义的权限map里有的资源才会去拦截,如果map中没有资源security不知道所以不拦截,也就是说我们新建或者修改资源路径后 在执行AccessDecisionManager的decide()方法时是无法获取到新建或者修改后的资源的,重启服务后map重新加载资源才可以在decide()里面获取到.所以关键就是map要更新资源,所以每次修改新建资源后一定要记得清空FilterInvocationSecurityMetadataSource中的map使它能够再次初始化,这样就解决了重启服务才能刷新资源的问题.