diff --git a/build.gradle b/build.gradle index 30dd5a8..2260eaa 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' compileOnly 'org.projectlombok:lombok' runtimeOnly 'mysql:mysql-connector-java' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/com/aiprose/scauth/conf/WebSecurityConfig.java b/src/main/java/com/aiprose/scauth/conf/WebSecurityConfig.java index 3abb4e2..821044e 100644 --- a/src/main/java/com/aiprose/scauth/conf/WebSecurityConfig.java +++ b/src/main/java/com/aiprose/scauth/conf/WebSecurityConfig.java @@ -1,12 +1,17 @@ package com.aiprose.scauth.conf; import com.aiprose.scauth.entity.User; -import com.aiprose.scauth.handler.AuthLimitHandler; -import com.aiprose.scauth.handler.LoginSuccessHandler; +import com.aiprose.scauth.handler.*; import com.aiprose.scauth.service.IUserService; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.AccessDecisionManager; +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.access.vote.AuthenticatedVoter; +import org.springframework.security.access.vote.RoleVoter; +import org.springframework.security.access.vote.UnanimousBased; +import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -15,6 +20,11 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.access.expression.WebExpressionVoter; +import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; + +import java.util.ArrayList; +import java.util.List; /** * @author nelson @@ -29,6 +39,9 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private IUserService userService; + @Autowired + private RemeberMeHandler remeberMeHandler; + @Override public void configure(WebSecurity web) throws Exception { // super.configure(web); @@ -38,19 +51,41 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); -// http.authorizeRequests().antMatchers("/user/save").permitAll().anyRequest(); + // 配置记住我的参数和记住我处理类 + http.rememberMe() + .tokenRepository(remeberMeHandler) + .tokenValiditySeconds(60*60*24) + .userDetailsService(userDetailsService()); + + // 配置登录页面 + http.formLogin().loginPage("/login").permitAll(); + // 配置登录成功后的操作 http.formLogin().successHandler(new LoginSuccessHandler()); //权限不足 http.exceptionHandling().accessDeniedHandler(new AuthLimitHandler()); - super.configure(http); + + // 登出授权 + http.logout().permitAll(); + + // 授权配置 + http.authorizeRequests() + /* 所有静态文件可以访问 */ + .antMatchers("/js/**","/css/**","/images/**").permitAll() + /* 所有 以/ad 开头的 广告页面可以访问 */ + .antMatchers("/ad/**").permitAll() + .antMatchers("/user/**","/role/**").permitAll() + /* 动态url权限 */ + .withObjectPostProcessor(new DefinedObjectPostProcessor()) + /* url决策 */ + .accessDecisionManager(accessDecisionManager()) + .anyRequest().authenticated(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder()); -// super.configure(auth); // auth.passwordEncoder(new BCryptPasswordEncoder()); // .withUser("nelson").password(new BCryptPasswordEncoder().encode("123456")).roles("admin") // .and() @@ -74,4 +109,33 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { return user; }; } + + /** + * AffirmativeBased – 任何一个AccessDecisionVoter返回同意则允许访问 + * ConsensusBased – 同意投票多于拒绝投票(忽略弃权回答)则允许访问 + * UnanimousBased – 每个投票者选择弃权或同意则允许访问 + * + * 决策管理 + */ + private AccessDecisionManager accessDecisionManager() { + List> decisionVoters = new ArrayList<>(); + decisionVoters.add(new WebExpressionVoter()); + decisionVoters.add(new AuthenticatedVoter()); + decisionVoters.add(new RoleVoter()); + /* 路由权限管理 */ + decisionVoters.add(new UrlRoleAuthHandler()); + return new UnanimousBased(decisionVoters); + } + + @Autowired + private UrlRolesFilterHandler urlRolesFilterHandler; + + + class DefinedObjectPostProcessor implements ObjectPostProcessor { + @Override + public O postProcess(O object) { + object.setSecurityMetadataSource(urlRolesFilterHandler); + return object; + } + } } diff --git a/src/main/java/com/aiprose/scauth/controller/HomeController.java b/src/main/java/com/aiprose/scauth/controller/HomeController.java new file mode 100644 index 0000000..1a1d7a3 --- /dev/null +++ b/src/main/java/com/aiprose/scauth/controller/HomeController.java @@ -0,0 +1,13 @@ +package com.aiprose.scauth.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class HomeController { + + @GetMapping("/home") + public String home(){ + return "home"; + } +} \ No newline at end of file diff --git a/src/main/java/com/aiprose/scauth/controller/LoginController.java b/src/main/java/com/aiprose/scauth/controller/LoginController.java new file mode 100644 index 0000000..b92c314 --- /dev/null +++ b/src/main/java/com/aiprose/scauth/controller/LoginController.java @@ -0,0 +1,13 @@ +package com.aiprose.scauth.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class LoginController { + + @GetMapping("/login") + public String login(){ + return "login"; + } +} \ No newline at end of file diff --git a/src/main/java/com/aiprose/scauth/entity/Menu.java b/src/main/java/com/aiprose/scauth/entity/Menu.java new file mode 100644 index 0000000..8e90927 --- /dev/null +++ b/src/main/java/com/aiprose/scauth/entity/Menu.java @@ -0,0 +1,21 @@ +package com.aiprose.scauth.entity; + +import lombok.Data; +import org.springframework.security.core.GrantedAuthority; + +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * @author nelson + * @desc 角色表 + * @company 北京中经网软件有限公司 + * @date 2020/11/27 17:04 + * @since 1.0 + */ +@Data +@Entity +@Table(name = "sys_menu") +public class Menu extends IDEntity { + private String url; +} diff --git a/src/main/java/com/aiprose/scauth/entity/RoleMenu.java b/src/main/java/com/aiprose/scauth/entity/RoleMenu.java new file mode 100644 index 0000000..e6ba6be --- /dev/null +++ b/src/main/java/com/aiprose/scauth/entity/RoleMenu.java @@ -0,0 +1,22 @@ +package com.aiprose.scauth.entity; + +import lombok.Data; + +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * @author nelson + * @desc 角色菜单关联表 + * @company 北京中经网软件有限公司 + * @date 2020/11/27 17:04 + * @since 1.0 + */ +@Data +@Entity +@Table(name="sys_role_menu") +public class RoleMenu extends IDEntity{ + private Integer sysRoleId; + private Integer sysMenuId; + +} diff --git a/src/main/java/com/aiprose/scauth/handler/LoginSuccessHandler.java b/src/main/java/com/aiprose/scauth/handler/LoginSuccessHandler.java index ffde943..13a9019 100644 --- a/src/main/java/com/aiprose/scauth/handler/LoginSuccessHandler.java +++ b/src/main/java/com/aiprose/scauth/handler/LoginSuccessHandler.java @@ -23,5 +23,6 @@ public class LoginSuccessHandler implements AuthenticationSuccessHandler { System.out.println(authentication.getAuthorities()); System.out.println(authentication.getCredentials()); System.out.println(authentication.getPrincipal()); + response.sendRedirect("/home"); } } diff --git a/src/main/java/com/aiprose/scauth/handler/RemeberMeHandler.java b/src/main/java/com/aiprose/scauth/handler/RemeberMeHandler.java new file mode 100644 index 0000000..eb21a8f --- /dev/null +++ b/src/main/java/com/aiprose/scauth/handler/RemeberMeHandler.java @@ -0,0 +1,103 @@ +package com.aiprose.scauth.handler; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken; +import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Component +public class RemeberMeHandler implements PersistentTokenRepository { + /** + * token有效时间30天 + */ + private static final Long TOKEN_VALID_DAYS = 15L; + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Override + public void createNewToken(PersistentRememberMeToken token) { + String key = generateTokenKey(token.getSeries()); + Map map = new HashMap<>(8); + map.put("username", token.getUsername()); + map.put("tokenValue", token.getTokenValue()); + map.put("date", String.valueOf(token.getDate().getTime())); + stringRedisTemplate.opsForHash().putAll(key, map); + stringRedisTemplate.expire(key,TOKEN_VALID_DAYS, TimeUnit.DAYS); + + saveUsernameAndSeries(token.getUsername(), token.getSeries()); + } + + @Override + public void updateToken(String series, String tokenValue, Date lastUsed) { + String key = generateTokenKey(series); + Boolean hasSeries = stringRedisTemplate.hasKey(key); + if(hasSeries==null || !hasSeries){ + return; + } + Map map = new HashMap<>(4); + map.put("tokenValue", tokenValue); + map.put("date", String.valueOf(lastUsed.getTime())); + stringRedisTemplate.opsForHash().putAll(key, map); + stringRedisTemplate.expire(key, TOKEN_VALID_DAYS, TimeUnit.DAYS); + + String username = stringRedisTemplate.opsForValue().get(generateUsernameAndSeriesKey(series)); + saveUsernameAndSeries(username, series); + } + + @Override + public PersistentRememberMeToken getTokenForSeries(String seriesId) { + String key = generateTokenKey(seriesId); + Map hashKeyValues = stringRedisTemplate.opsForHash().entries(key); + if (hashKeyValues == null) { + return null; + } + Object username = hashKeyValues.get("username"); + Object tokenValue = hashKeyValues.get("tokenValue"); + Object date = hashKeyValues.get("date"); + if (null == username || null == tokenValue || null == date) { + return null; + } + long timestamp = Long.valueOf(String.valueOf(date)); + Date time = new Date(timestamp); + + return new PersistentRememberMeToken(String.valueOf(username), seriesId, String.valueOf(tokenValue), time); + } + + @Override + public void removeUserTokens(String username) { + String series = stringRedisTemplate.opsForValue().get(generateUsernameAndSeriesKey(username)); + if (series==null||series.trim().length()<=0){ + return; + } + stringRedisTemplate.delete(generateTokenKey(series)); + stringRedisTemplate.delete(generateUsernameAndSeriesKey(username)); + stringRedisTemplate.delete(generateUsernameAndSeriesKey(series)); + + } + + private void saveUsernameAndSeries(String username, String series){ + stringRedisTemplate.opsForValue().set(generateUsernameAndSeriesKey(username),series,TOKEN_VALID_DAYS*2, TimeUnit.DAYS); + stringRedisTemplate.opsForValue().set(generateUsernameAndSeriesKey(series),username,TOKEN_VALID_DAYS*2, TimeUnit.DAYS); + } + + /** + * 生成token key + */ + private String generateTokenKey(String series) { + return "spring:security:rememberMe:token:" + series; + } + + /** + * 生成key + */ + private String generateUsernameAndSeriesKey(String usernameOrSeries) { + return "spring:security:rememberMe:"+usernameOrSeries; + } +} diff --git a/src/main/java/com/aiprose/scauth/handler/UrlRoleAuthHandler.java b/src/main/java/com/aiprose/scauth/handler/UrlRoleAuthHandler.java new file mode 100644 index 0000000..5ea4d7e --- /dev/null +++ b/src/main/java/com/aiprose/scauth/handler/UrlRoleAuthHandler.java @@ -0,0 +1,58 @@ +package com.aiprose.scauth.handler; + +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * 角色 权限 路由处理 + */ +public class UrlRoleAuthHandler implements AccessDecisionVoter { + @Override + public boolean supports(ConfigAttribute attribute) { + if (null == attribute.getAttribute()) { + return false; + } + return true; + } + + @Override + public boolean supports(Class clazz) { + return true; + } + + /** + * ACCESS_GRANTED – 同意 + * ACCESS_DENIED – 拒绝 + * ACCESS_ABSTAIN – 弃权 + */ + @Override + public int vote(Authentication user, Object object, Collection urlRoles) { + if (null == user) { + return ACCESS_DENIED; + } + + int result = ACCESS_ABSTAIN; + Collection userRoles = user.getAuthorities(); + + /* 遍历链接中对应的权限 */ + for (ConfigAttribute urlRole : urlRoles) { + if (this.supports(urlRole)) { + /* 此处默认值为弃权,表示只要有一个角色对应上,用户就可以访问链接 + 如果值改为拒绝,表示必须全部角色包含才能访问链接 + */ + result = ACCESS_ABSTAIN; + /* 遍历用户中对应的角色列表 */ + for (GrantedAuthority userRole : userRoles) { + if (urlRole.getAttribute().equals(userRole.getAuthority())) { + return ACCESS_GRANTED; + } + } + } + } + return result; + } +} diff --git a/src/main/java/com/aiprose/scauth/handler/UrlRolesFilterHandler.java b/src/main/java/com/aiprose/scauth/handler/UrlRolesFilterHandler.java new file mode 100644 index 0000000..0d5d74e --- /dev/null +++ b/src/main/java/com/aiprose/scauth/handler/UrlRolesFilterHandler.java @@ -0,0 +1,40 @@ +package com.aiprose.scauth.handler; + +import com.aiprose.scauth.service.IRoleService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.access.SecurityConfig; +import org.springframework.security.web.FilterInvocation; +import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; + +@Service +public class UrlRolesFilterHandler implements FilterInvocationSecurityMetadataSource { + + @Autowired + private IRoleService roleService; + + @Override + public Collection getAttributes(Object object) throws IllegalArgumentException { + String requestUrl = ((FilterInvocation) object).getRequestUrl(); + List roleNames = roleService.findByUrl(requestUrl); + String[] names = new String[roleNames.size()]; + for (int i = 0; i < roleNames.size(); i++) { + names[i] = roleNames.get(i); + } + return SecurityConfig.createList(names); + } + + @Override + public Collection getAllConfigAttributes() { + return null; + } + + @Override + public boolean supports(Class clazz) { + return FilterInvocation.class.isAssignableFrom(clazz); + } +} diff --git a/src/main/java/com/aiprose/scauth/repository/MenuRepository.java b/src/main/java/com/aiprose/scauth/repository/MenuRepository.java new file mode 100644 index 0000000..2a6373b --- /dev/null +++ b/src/main/java/com/aiprose/scauth/repository/MenuRepository.java @@ -0,0 +1,18 @@ +package com.aiprose.scauth.repository; + +import com.aiprose.scauth.entity.Menu; +import com.aiprose.scauth.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +/** + * @author nelson + * @desc TODO + * @company 北京中经网软件有限公司 + * @date 2020/11/27 17:17 + * @since 1.0 + */ +public interface MenuRepository extends JpaRepository, JpaSpecificationExecutor { + + Menu findByUrl(String url); +} diff --git a/src/main/java/com/aiprose/scauth/repository/RoleMenuRepository.java b/src/main/java/com/aiprose/scauth/repository/RoleMenuRepository.java new file mode 100644 index 0000000..c86e763 --- /dev/null +++ b/src/main/java/com/aiprose/scauth/repository/RoleMenuRepository.java @@ -0,0 +1,20 @@ +package com.aiprose.scauth.repository; + +import com.aiprose.scauth.entity.RoleMenu; +import com.aiprose.scauth.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.util.List; + +/** + * @author nelson + * @desc TODO + * @company 北京中经网软件有限公司 + * @date 2020/11/27 17:17 + * @since 1.0 + */ +public interface RoleMenuRepository extends JpaRepository, JpaSpecificationExecutor { + + List findBySysMenuId(Integer menuId); +} diff --git a/src/main/java/com/aiprose/scauth/service/IRoleService.java b/src/main/java/com/aiprose/scauth/service/IRoleService.java index eb87a14..ff9de39 100644 --- a/src/main/java/com/aiprose/scauth/service/IRoleService.java +++ b/src/main/java/com/aiprose/scauth/service/IRoleService.java @@ -16,5 +16,7 @@ import java.util.List; public interface IRoleService extends BaseSpecService { List findByUserId(Integer userId); + List findByUrl(String url); + UserRole save(Role role, Integer userId); } diff --git a/src/main/java/com/aiprose/scauth/service/impl/RoleServiceImpl.java b/src/main/java/com/aiprose/scauth/service/impl/RoleServiceImpl.java index 3fd6e04..c881e6b 100644 --- a/src/main/java/com/aiprose/scauth/service/impl/RoleServiceImpl.java +++ b/src/main/java/com/aiprose/scauth/service/impl/RoleServiceImpl.java @@ -1,8 +1,12 @@ package com.aiprose.scauth.service.impl; import com.aiprose.base.BaseSpecServiceImpl; +import com.aiprose.scauth.entity.Menu; import com.aiprose.scauth.entity.Role; +import com.aiprose.scauth.entity.RoleMenu; import com.aiprose.scauth.entity.UserRole; +import com.aiprose.scauth.repository.MenuRepository; +import com.aiprose.scauth.repository.RoleMenuRepository; import com.aiprose.scauth.repository.RoleRepository; import com.aiprose.scauth.repository.UserRoleRepository; import com.aiprose.scauth.service.IRoleService; @@ -28,6 +32,12 @@ public class RoleServiceImpl extends BaseSpecServiceImpl findByUserId(Integer userId) { List userRoles = userRoleRepository.findBySysUserId(userId); @@ -42,6 +52,22 @@ public class RoleServiceImpl extends BaseSpecServiceImpl(); } + @Override + public List findByUrl(String url) { + Menu menu = menuRepository.findByUrl(url); + if(menu == null){ + return new ArrayList<>(); + } + List bySysMenuId = roleMenuRepository.findBySysMenuId(menu.getId()); + if(bySysMenuId == null){ + return new ArrayList<>(); + } + List roleIds = bySysMenuId.stream().map(x -> x.getSysRoleId()).collect(Collectors.toList()); + List roles = repository.findAllById(roleIds); + List roleNames = roles.stream().map(x -> x.getRole()).collect(Collectors.toList()); + return roleNames; + } + @Override public UserRole save(Role role, Integer userId) { Role newRole = repository.save(role); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4c39639..9a0786f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,10 @@ server: port: 9982 spring: + redis: + host: localhost + port: 6379 + database: 1 jpa: hibernate: ddl-auto: update diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..4bad8de --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,30 @@ + + + + 请登录 + + +
+
+

+ 用户名: + +

+

+ 密码: + +

+ +

+ 记住我: + +

+ + + + + +
+
+ + \ No newline at end of file