@File
2019-10-19T07:28:07.000000Z
字数 20162
阅读 151
java
<!-- security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- social --><dependency><groupId>org.springframework.social</groupId><artifactId>spring-social-web</artifactId><version>1.1.6.RELEASE</version></dependency><!-- jdbc --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency>
spring:datasource:url: jdbc:mysql://47.107.167.205:8888/test?useSSL=false&serverTimezone=UTCusername: rootpassword:driver-class-name: com.mysql.cj.jdbc.Driver
@Configurationpublic class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {@Resourceprivate DataSource dataSource;/*** 查询逻辑*/@Resourceprivate UserSecurityService userSecurityService;/*** 验证码过滤类*/@Resourceprivate ImageCodeValidateFilter imageCodeValidateFilter;/*** 继承 `WebSecurityConfigurerAdapter` 并重写 `configure` 方法*/@Overrideprotected void configure(HttpSecurity http) throws Exception {// 请求前执行过滤(四、7. 验证码过滤)http.addFilterBefore(imageCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)// ====== 基础登录设置 ====== //.formLogin() // 采用表单登录.loginPage("/login.html") // 登录页面.loginProcessingUrl("/authentication/form") // 请求接口(不用实现,自动调用 四、1.登录)// 成功逻辑(可调用 四、3.登录成功类).successHandler((req,resp,exception) -> {resp.setContentType("applicatoin/json;charset=utf-8");resp.getWriter().write("登录成功");})// 失败逻辑(可调用 四、4.登录失败类).failureHandler((req,resp,execption) -> {resp.setContentType("applicatoin/json;charset=utf-8");resp.getWriter().write("登录失败");})// ====== 退出登录 ====== //.and().logout() // 开启访问 /logout 实现功能.logoutSuccessUrl("/login.html") // 退出后跳转的页面// ====== 权限认证(可选) ====== //.and() // 功能分隔,表示进行其他的配置.authorizeRequests() // 表示所有的都需要认证.antMatchers("/login.html", "/js/**", "/css/**") // 白名单.permitAll().anyRequest() // 对于所有的请求.authenticated() // 认证后才能访问// ====== 记住登录状态(可选) ====== //// 前端传参 remember-me:true|false.and().rememberMe() // 功能开启.tokenValiditySeconds(360000) // 记录时长.tokenRepository(persistentTokenRepository()) // 指定token库.userDetailsService(userSecurityService)// .alwaysRemember(true) // true 时只能记住// ====== 关闭跨站请求伪造功能(必须) ====== //.and().csrf().disable();}/*** 配置密码加密规则*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 配置token验证*/@Beanpublic PersistentTokenRepository persistentTokenRepository() {JdbcTokenRepositoryImpl jdbcTokenRepository= new JdbcTokenRepositoryImpl();jdbcTokenRepository.setDataSource(dataSource);// 是否自动建表jdbcTokenRepository.setCreateTableOnStartup(false);return jdbcTokenRepository;}}
UserDetailsService 接口
@Componentpublic class UserSecurityService implements UserDetailsService {/*** service 层*/@Resourceprivate SysUserService sysUserService;@Overridepublic UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {// 通过用户名查出数据(三、2.通过用户名获取数据)SysUser user = sysUserService.getUser(name);// 通过查询结果的密码做比对User admin = new User(// 登录名name,// 用户密码user.getPassword(),// 权限(用于 八、权限验证),必须 ROLE_ 前缀Arrays.asList(new SimpleGrantedAuthority("ROLE_admin")));return admin;}}
@Servicepublic class SysUserService {@Resourceprivate JdbcTemplate jdbcTemplate;public SysUser getUser(String name) {String sql = "SELECT * FROM `sys_user` WHERE `username` = ?";SysUser sysUser = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(SysUser.class), name);return sysUser;}}
AuthenticationSuccessHandler 接口
@Componentpublic class MySuccessAuthenticationHandler implements AuthenticationSuccessHandler {/*** 登陆成功* @param request 请求对象* @param response 响应对象* @param authentication* @throws IOException* @throws ServletException*/@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {// 成功逻辑response.setContentType("applicatoin/json;charset=utf-8");response.getWriter().write("登录成功");}}
AuthenticationFailureHandler 接口
@Componentpublic class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {/*** 登陆失败* @param request 请求对象* @param response 响应对象* @param exception 失败和异常信息* @throws IOException* @throws ServletException*/@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {// 失败逻辑response.setContentType("applicatoin/json;charset=utf-8");response.getWriter().write("登录成功");}}
@Dataabstract class ValidationCode implements Serializable {/*** 验证码*/private String code;/*** 有效时间*/private LocalDateTime expire;/*** 有参构造* @param seconds 验证码存活时间*/ValidationCode(int seconds) {// 执行子类的验证码生成逻辑code = setValidationCode();// 赋予存活时间setExpire(seconds);}/*** 验证码生成逻辑*/abstract protected String setValidationCode();/*** 判断验证时间是否有效* @return true | false*/public boolean isExpire() {return LocalDateTime.now().isAfter(getExpire());}/*** 修改有效时间* @param seconds 秒*/public void setExpire(int seconds) {this.expire = LocalDateTime.now().plusSeconds(seconds);}}
setValidationCode()
@Datapublic class ImageCode extends ValidationCode {/*** 验证码图片本身*/private BufferedImage image;/*** 构造方法直接继承父类* @param seconds 存活时间*/public ImageCode(int seconds) {super(seconds);}/*** 编写生成图片验证码* @return 验证码*/@Overrideprotected String setValidationCode() {int width = 67;int height = 23;BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);Graphics g = image.getGraphics();Random random = new Random();g.setColor(getRandColor(200, 250));g.fillRect(0, 0, width, height);g.setFont(new Font("Times New Roman", Font.ITALIC, 20));g.setColor(getRandColor(160, 200));for (int i = 0; i < 155; i++) {int x = random.nextInt(width);int y = random.nextInt(height);int xl = random.nextInt(12);int yl = random.nextInt(12);g.drawLine(x, y, x + xl, y + yl);}// 随机生成文字String sRand = "";for (int i = 0; i < 4; i++) {String rand = String.valueOf(random.nextInt(10));sRand += rand;g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));g.drawString(rand, 13 * i + 6, 16);}g.dispose();// 修改图片信息setImage(image);// 给父类返回验证码return sRand;}/*** 生成随机背景条纹** @param fc rgb色值* @param bc rgb色值* @return*/private Color getRandColor(int fc, int bc) {Random random = new Random();if (fc > 255) {fc = 255;}if (bc > 255) {bc = 255;}int r = fc + random.nextInt(bc - fc);int g = fc + random.nextInt(bc - fc);int b = fc + random.nextInt(bc - fc);return new Color(r, g, b);}}
OncePerRequestFilter 一次过滤
@Componentpublic class ImageCodeValidateFilter extends OncePerRequestFilter {/*** session 工具类*/private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();@Overrideprotected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain fc) throws ServletException, IOException {// 只应用于 /authentication/form 的 postif(req.getMethod().equals("POST") && "/authentication/form".equals(req.getRequestURI())) {// 取到验证码ImageCode imageCode = (ImageCode)sessionStrategy.getAttribute(new ServletWebRequest(req),ValidataCodeController.VALIDATE_CODE_KEY);// 验证码是否过期 || 验证码是否正确if(imageCode.isExpire() || !Predicate.isEqual(imageCode.getCode()).test(req.getParameter("validateCode"))){// 验证失败resp.getWriter().write("error");return;}}// 验证成功,继续执行后续代码fc.doFilter(req, resp);}}
@Controller@RequestMapping("/validate")public class ImageCodeController {/*** session 的工具类*/private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();/*** session 键名*/public static final String VALIDATE_CODE_KEY = "IMAGE_CODE_KEY";/*** 获取验证码接口* @param req 请求对象* @param resp 响应对象* @throws IOException*/@GetMapping("/code")public void validateCode(HttpServletRequest req, HttpServletResponse resp) throws IOException {// 生成验证码ImageCode imageCode = new ImageCode(60);// 将ImageCode存入到sessionsessionStrategy.setAttribute(new ServletWebRequest(req), VALIDATE_CODE_KEY, imageCode);//将图片写入前端ImageIO.write(imageCode.getImage(), "JPEG", resp.getOutputStream());}}
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-text</artifactId><version>1.8</version></dependency><!-- 阿里云发送短信 --><dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-core</artifactId><version>4.0.3</version></dependency>
UsernamePasswordAuthenticationFilter
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {// ~ Static fields/initializers// =====================================================================================public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;private boolean postOnly = true;// ~ Constructors// ===================================================================================================public SmsAuthenticationFilter() {super(new AntPathRequestMatcher("/authentication/sms", "POST"));}// ~ Methods// ========================================================================================================@Overridepublic Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {if (postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}String mobile = obtainMobile(request);if (mobile == null) {mobile = "";}// 反射mobile = mobile.trim();SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);// Allow subclasses to set the "details" propertysetDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}protected String obtainMobile(HttpServletRequest request) {return request.getParameter(mobileParameter);}/*** Provided so that subclasses may configure what is put into the authentication* request's details property.** @param request that an authentication request is being created for* @param authRequest the authentication request object that should have its details* set*/protected void setDetails(HttpServletRequest request,SmsAuthenticationToken authRequest) {authRequest.setDetails(authenticationDetailsSource.buildDetails(request));}/*** Sets the parameter name which will be used to obtain the username from the login* request.** @param mobileParameter the parameter name. Defaults to "username".*/public void setUsernameParameter(String mobileParameter) {Assert.hasText(mobileParameter, "Username parameter must not be empty or null");this.mobileParameter = mobileParameter;}/*** Defines whether only HTTP POST requests will be allowed by this filter. If set to* true, and an authentication request is received which is not a POST request, an* exception will be raised immediately and authentication will not be attempted. The* <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed* authentication.* <p>* Defaults to <tt>true</tt> but may be overridden by subclasses.*/public void setPostOnly(boolean postOnly) {this.postOnly = postOnly;}public final String getMobileParameter() {return mobileParameter;}}
@Componentpublic class SmsCodeValidateFilter extends OncePerRequestFilter {/*** session 工具类*/private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();@Overrideprotected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain fc) throws ServletException, IOException {// 只应用于 /authentication/sms 的 postif(req.getMethod().equals("POST") && "/authentication/sms".equals(req.getRequestURI())) {// 取到验证码SmsCode smsCode = (SmsCode)sessionStrategy.getAttribute(new ServletWebRequest(req), SmsCodeController.VALIDATE_CODE_KEY);// 验证码是否过期 || 验证码是否正确if(smsCode.isExpire() || !Predicate.isEqual(smsCode.getCode()).test(req.getParameter("smsCode"))){// 验证失败resp.getWriter().write("error");return;}}// 验证成功,继续执行后续代码fc.doFilter(req, resp);}}
UsernamePasswordAuthenticationToken
public class SmsAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;// ~ Instance fields// ================================================================================================/*** 在验证之前封装电话信息,* 在验证之后存储 用户信息*/private final Object principal;// ~ Constructors// ===================================================================================================/*** This constructor can be safely used by any code that wishes to create a* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}* will return <code>false</code>.*/public SmsAuthenticationToken(Object principal) {super(null);this.principal = principal;setAuthenticated(false);}/*** This constructor should only be used by <code>AuthenticationManager</code> or* <code>AuthenticationProvider</code> implementations that are satisfied with* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)* authentication token.** @param principal* @param authorities*/public SmsAuthenticationToken(Object principal,Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;super.setAuthenticated(true); // must use super, as we override}@Overridepublic Object getCredentials() {return null;}@Overridepublic Object getPrincipal() {return this.principal;}@Overridepublic void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {if (isAuthenticated) {throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");}super.setAuthenticated(false);}@Overridepublic void eraseCredentials() {super.eraseCredentials();}}
AuthenticationProvider 接口
public class SmsAuthenticatoinProvider implements AuthenticationProvider {private UserSecurityService userSecurityService;/**** @param authentication* @return* @throws AuthenticationException*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {SmsAuthenticationToken smsAuthenticationToken = (SmsAuthenticationToken)authentication;// 获取到电话String mobile = (String)smsAuthenticationToken.getPrincipal();UserDetails userDetails = userSecurityService.loadUserByUsername(mobile);SmsAuthenticationToken token = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());// 设置用户的其他的详细信息(登录一些)token.setDetails(smsAuthenticationToken.getDetails());return token;}/*** 该方法就是来判断,该Provider要处理哪一个Filter丢过来的Token,* 返回true, 上面的方法 authenticate(Authentication authentication)*/@Overridepublic boolean supports(Class<?> authentication) {// 判断类型是否一致return authentication.isAssignableFrom(SmsAuthenticationToken.class);}}
SecurityConfigurerAdapter
@Configurationpublic class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain,HttpSecurity> {/*** 登录逻辑*/@Resourceprivate UserSecurityService userSecurityService;/*** 登录成功*/@Resourceprivate MySuccessAuthenticationHandler successHandler;/*** 登录失败*/@Resourceprivate MyAuthenticationFailureHandler failureHandler;/*** 配置* @param http 配置对象* @throws Exception*/@Overridepublic void configure(HttpSecurity http) throws Exception {// 创建自定义过滤实例SmsAuthenticationFilter filter = new SmsAuthenticationFilter();// 创建自定义provider实例SmsAuthenticatoinProvider provider = new SmsAuthenticatoinProvider();// 设置AuthenticatoinManager, 因为filter和Provider中间的桥梁就是 AuthenticationManagerfilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));// 登录成功filter.setAuthenticationSuccessHandler(successHandler);// 登录失败filter.setAuthenticationFailureHandler(failureHandler);// 绑定登录查询逻辑provider.setUserSecurityService(userSecurityService);// 绑定providerhttp.authenticationProvider(provider)// 绑定过滤器.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);}}
@Configurationpublic class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {/*** 自定义的配置类*/@Resourceprivate SmsAuthenticationConfig smsAuthenticationConfig;/*** 手机验证码过滤器*/@Resourceprivate SmsCodeValidateFilter smsCodeValidateFilter;@Overrideprotected void configure(HttpSecurity http) throws Exception {// 三个修改http.addFilterBefore()// 添加手机验证码过滤器.addFilterBefore(smsCodeValidateFilter, UsernamePasswordAuthenticationFilter.class).xxx().and().xxx()// 记得设置白名单.antMatchers("/Sms/code").xxx().and().csrf().disable()// 在这里接入 apply().apply(smsAuthenticationConfig);}}
public class SmsCode extends ValidationCode {/*** 直接使用父类构造* @param seconds 存活时间*/public SmsCode(int seconds) {super(seconds);}/*** 定义验证码生成逻辑* @return 验证码*/@Overrideprotected String setValidationCode() {// 生成随机数子的工具类RandomStringGenerator randomStringGenerator= new RandomStringGenerator.Builder().withinRange(new char[]{'0','9'}).build();return randomStringGenerator.generate(4);}}
SmsService 为第三个接口调用逻辑,不公开展示
@Servicepublic class SysUserService {/*** 封装好请求第三方接口的类*/@Resourceprivate SmsService smsService;public boolean sendSms(String code) {return smsService.send(code+"","mobile");}}
@Controller@RequestMapping("/validate")public class SmsCodeController {/*** service 层*/@Resourceprivate SysUserService sysUserService;/*** session 的工具类*/private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();/*** session 键名*/public static final String VALIDATE_CODE_KEY = "Sms_CODE_KEY";/*** 获取验证码接口* @param req 请求对象* @param resp 响应对象* @throws IOException*/@GetMapping("/code")public void validateCode(HttpServletRequest req, HttpServletResponse resp) throws IOException {// 生成验证码SmsCode SmsCode = new SmsCode(60);// 将SmsCode存入到sessionsessionStrategy.setAttribute(new ServletWebRequest(req), VALIDATE_CODE_KEY, SmsCode);// 请求发送短信sysUserService.sendSms();}}
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-core</artifactId><version>2.1.9.RELEASE</version></dependency><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId><version>2.1.9.RELEASE</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.7.0</version></dependency>
spring:# 基本的 redis 配置redis:port: 6379host: localhostpassword:# 连接池lettuce:pool:min-idle: 2max-active: 8session:# session 存储方式store-type: redis
@Configurationpublic class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.xxx().and().sessionManagement() //.maximumSessions(1) // 表示一个用户只能有一个会话,不能重复登录// 被强制下线时执行的逻辑.expiredSessionStrategy(event -> {HttpServletResponse response = event.getResponse();response.setContentType("text/plain;charset=utf-8");response.getWriter().write("您已经在其他设备登录,强制下线");})// 要用两个 and() 做结束.and().and().xxx();}}
@Componentpublic class UserSecurityService implements UserDetailsService {/*** service 层*/@Resourceprivate SysUserService sysUserService;@Overridepublic UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {SysUser user = sysUserService.getUser(name);// 实例化 User 时第三个参数是 List 类型的权限集合User admin = new User(name, user.getPassword(),// 必须 ROLE_ 前缀,实际情况是通过数据动态生成的Arrays.asList(new SimpleGrantedAuthority("ROLE_admin"),new SimpleGrantedAuthority("ROLE_user")));return admin;}}
securedEnabled: 开启
@Secured("ROLE_abc")方式
jsr250Enabled: 开启@RolesAllowed("admin")方式
prePostEnabled: 开启@PreAuthorize和@PostAuthorize方式
@SpringBootApplication@EnableGlobalMethodSecurity(jsr250Enabled = true)public class SecurityApplication {public static void main(String[] args) {SpringApplication.run(SecurityApplication.class, args);}}
@Configurationpublic class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.xxx()// 方式一:用户权限控制(推荐可用注解配置).antMatchers("/user/**", "/dept/**").hasRole("admin") // 不能有ROLE_前缀// 方式二:操作权限控制(推荐用注解配置).antMatchers("/user/delete").hasRole("user:delete").and()// 无权限访问时的处理.exceptionHandling().accessDeniedHandler((req,resp,exception) -> {// 处理逻辑resp.sendRedirect("/noAuth.html");}).xxx();}}
@RolesAllowed jsr250方式
@RestController// 该类中所有方法都需要 user 权限@RolesAllowed("user")public class Demo {@GetMapping("/TestSecurity")// 该方法需要 admin 或 user 权限@RolesAllowed({"admin","user"})public String test() {return "test";}}
@Secured secured方式
public interface UserService {@Secured("ROLE_user")List<User> findAllUsers();@Secured("ROLE_admin")void deleteUser(int id);}
@PreAuthorize 方法执行前
/*** 对角色做权限控制*/@PreAuthorize("hasRole('admin')") // 需要角色 admin@PreAuthorize("hasRole('admin') or hasRole('user')") // 需要角色 admin 或 user@PreAuthorize("hasRole('admin') and hasRole('user')") // 同时需要角色 admin 和 user@PreAuthorize("hasAnyRole('admin','user')") // 需要角色 admin 或 user/*** 对操作做权限控制*/@PreAuthorize("hasAuthority('user:delete')") // 需要有 user:delete 操作权限@PreAuthorize("hasAuthority('user:delete') or hasAuthority('user:root')") // 需要有 user:delete 或 user:root 操作权限@PreAuthorize("hasAuthority('user:delete') and hasAuthority('user:root')") // 同时需要 user:delete 和 user:root 操作权限@PreAuthorize("hasAnyAuthority('user:delete','user:root')") // 需要有 user:delete 或 user:root 操作权限
@PostAuthorize 方法执行后@PreAuthorize 语法一样